diff --git a/csharp/App/DeligreenBatteryCommunication/DeligreenBatteryCommunication.csproj b/csharp/App/DeligreenBatteryCommunication/DeligreenBatteryCommunication.csproj
index 6ab0149e5..0017ee879 100644
--- a/csharp/App/DeligreenBatteryCommunication/DeligreenBatteryCommunication.csproj
+++ b/csharp/App/DeligreenBatteryCommunication/DeligreenBatteryCommunication.csproj
@@ -9,6 +9,7 @@
+
diff --git a/csharp/App/DeligreenBatteryCommunication/Program.cs b/csharp/App/DeligreenBatteryCommunication/Program.cs
index 9d73e76e2..469624ac4 100644
--- a/csharp/App/DeligreenBatteryCommunication/Program.cs
+++ b/csharp/App/DeligreenBatteryCommunication/Program.cs
@@ -1,10 +1,12 @@
using InnovEnergy.Lib.Devices.BatteryDeligreen;
+namespace InnovEnergy.App.DeligreenBatteryCommunication;
+
internal static class Program
{
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(2);
- // private static readonly Channel? BatteriesChannel;
+ // private static readonly Channel? BatteriesChannel;
private const String Port = "/dev/ttyUSB0";
@@ -13,34 +15,57 @@ internal static class Program
{
Console.WriteLine("Hello, Deligreen World!");
- // BatteriesChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits);
+ // BatteriesChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits);
}
public static async Task Main(string[] args)
{
- Console.WriteLine("Starting Battery Communication");
-
- var batteryDevices = new BatteryDeligreenDevice(Port);
-
- while (true)
+ var listOfBatteries = new List
{
+ new BatteryDeligreenDevice(Port, 0),
+ new BatteryDeligreenDevice(Port, 1),
+ new BatteryDeligreenDevice(Port, 2),
+ new BatteryDeligreenDevice(Port, 3),
+ new BatteryDeligreenDevice(Port, 4)
+ };
+
+ var batteryDevices = new BatteryDeligreenDevices(listOfBatteries);
+
+ Console.WriteLine("Starting Battery Communication");
+
+ while (true)
+ {
try
{
- Console.WriteLine("***************************** New Frame *********************************");
- Console.WriteLine($"First Reading Timestamp: {DateTime.Now:HH:mm:ss.fff}");
- // Read telemetry data asynchronously
- await batteryDevices.ReadTelemetryData();
- Console.WriteLine($"Last Timestamp: {DateTime.Now:HH:mm:ss.fff}");
+ var startTime = DateTime.Now;
+ Console.WriteLine("***************************** Reading Battery Data *********************************************");
+ Console.WriteLine($"Start Reading all Batteries: {startTime}");
+ var batteriesRecord = batteryDevices.Read();
+ var stopTime = DateTime.Now;
+ Console.WriteLine($"Finish Reading all Batteries: {stopTime}");
+
+ Console.WriteLine("Time used for reading all batteries:" + (stopTime - startTime));
+
+ Console.WriteLine("Average SOC " + batteriesRecord?.Soc);
+ Console.WriteLine("SOC Battery 0 : " + batteriesRecord?.Devices[0].BatteryDeligreenDataRecord.Soc);
+ Console.WriteLine("SOC Battery 1 : " + batteriesRecord?.Devices[1].BatteryDeligreenDataRecord.Soc);
+ Console.WriteLine("SOC Battery 2 : " + batteriesRecord?.Devices[2].BatteryDeligreenDataRecord.Soc);
+ Console.WriteLine("SOC Battery 3 : " + batteriesRecord?.Devices[3].BatteryDeligreenDataRecord.Soc);
+ Console.WriteLine("SOC Battery 4 : " + batteriesRecord?.Devices[4].BatteryDeligreenDataRecord.Soc);
+ Console.WriteLine("Min Soc " + batteriesRecord?.CurrentMinSoc);
+ Console.WriteLine("count " + batteriesRecord?.Devices.Count);
+
// Wait for 2 seconds before the next reading
await Task.Delay(2000); // Delay in milliseconds (2000ms = 2 seconds)
}
catch (Exception e)
{
// Handle exception and print the error
- Console.WriteLine(e);
+ Console.WriteLine(e + " This the first try loop ");
await Task.Delay(2000); // Delay in milliseconds (2000ms = 2 seconds)
}
}
}
+
}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/Doc/SalimaxConfigReadme.txt b/csharp/App/SodiStoreMax/Doc/SalimaxConfigReadme.txt
new file mode 100644
index 000000000..559eb6ca8
--- /dev/null
+++ b/csharp/App/SodiStoreMax/Doc/SalimaxConfigReadme.txt
@@ -0,0 +1,110 @@
+"MinSoc": Number, 0 - 100 this is the minimum State of Charge that the batteries must not go below,
+ "ForceCalibrationCharge": Boolean (true or false), A flag to force a calibration charge,
+ "DisplayIndividualBatteries": Boolean (true or false), To display the indvidual batteries
+ "PConstant": Number 0 - 1, P value of our controller.
+ "GridSetPoint": Number in Watts, The set point of our controller.
+ "BatterySelfDischargePower": Number, 200, this a physical measurement of the self discharging power.
+ "HoldSocZone": Number, 1, This is magic number for the soft landing factor.
+ "IslandMode": { // Dc Link Voltage in Island mode
+ "AcDc": {
+ "MaxDcLinkVoltage": Number, 810, Max Dc Link Voltage,
+ "MinDcLinkVoltage": Number, 690, Min Dc Link Voltage,
+ "ReferenceDcLinkVoltage": Number, 750, Reference Dc Link
+ },
+ "DcDc": {
+ "LowerDcLinkVoltage": Number, 50, Lower Dc Link Window ,
+ "ReferenceDcLinkVoltage": 750, reference Dc Link
+ "UpperDcLinkVoltage": Number, 50, Upper Dc Link Window ,
+ }
+ },
+ "GridTie": {// Dc Link Voltage in GrieTie mode
+ "AcDc": {
+ "MaxDcLinkVoltage":Number, 780, Max Dc Link Voltage,
+ "MinDcLinkVoltage": Number, 690, Min Dc Link Voltage,
+ "ReferenceDcLinkVoltage": Number, 750, Reference Dc Link
+ },
+ "DcDc": {
+ "LowerDcLinkVoltage": Number, 20, Lower Dc Link Window ,
+ "ReferenceDcLinkVoltage": 750, reference Dc Link
+ "UpperDcLinkVoltage": Number, 20, Upper Dc Link Window ,
+ }
+ },
+ "MaxBatteryChargingCurrent":Number, 0 - 210, Max Charging current by DcDc
+ "MaxBatteryDischargingCurrent":Number, 0 - 210, Max Discharging current by DcDc
+ "MaxDcPower": Number, 0 - 10000, Max Power exported/imported by DcDc (10000 is the maximum)
+ "MaxChargeBatteryVoltage": Number, 57, Max Charging battery Voltage
+ "MinDischargeBatteryVoltage": Number, 0, Min Charging Battery Voltage
+ "Devices": { This is All Salimax devices (including offline ones)
+ "RelaysIp": {
+ "DeviceState": 1, // 0: is not present, 1: Present and Can be mesured, 2: Present but must be computed/calculted
+ "Host": "10.0.1.1", // Ip @ of the device in the local network
+ "Port": 502 // port
+ },
+ "GridMeterIp": {
+ "DeviceState": 1,
+ "Host": "10.0.4.1",
+ "Port": 502
+ },
+ "PvOnAcGrid": {
+ "DeviceState": 0, // If a device is not present
+ "Host": "false", // this is not important
+ "Port": 0 // this is not important
+ },
+ "LoadOnAcGrid": {
+ "DeviceState": 2, // this is a computed device
+ "Host": "true",
+ "Port": 0
+ },
+ "PvOnAcIsland": {
+ "DeviceState": 0,
+ "Host": "false",
+ "Port": 0
+ },
+ "IslandBusLoadMeterIp": {
+ "DeviceState": 1,
+ "Host": "10.0.4.2",
+ "Port": 502
+ },
+ "TruConvertAcIp": {
+ "DeviceState": 1,
+ "Host": "10.0.2.1",
+ "Port": 502
+ },
+ "PvOnDc": {
+ "DeviceState": 1,
+ "Host": "10.0.5.1",
+ "Port": 502
+ },
+ "LoadOnDc": {
+ "DeviceState": 0,
+ "Host": "false",
+ "Port": 0
+ },
+ "TruConvertDcIp": {
+ "DeviceState": 1,
+ "Host": "10.0.3.1",
+ "Port": 502
+ },
+ "BatteryIp": {
+ "DeviceState": 1,
+ "Host": "localhost",
+ "Port": 6855
+ },
+ "BatteryNodes": [ // this is a list of nodes
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ]
+ },
+ "S3": { // this is parameters of S3 Buckets and co
+ "Bucket": "8-3e5b3069-214a-43ee-8d85-57d72000c19d",
+ "Region": "sos-ch-dk-2",
+ "Provider": "exo.io",
+ "Key": "EXO502627299197f83e8b090f63",
+ "Secret": "jUNYJL6B23WjndJnJlgJj4rc1i7uh981u5Aba5xdA5s",
+ "ContentType": "text/plain; charset=utf-8",
+ "Host": "8-3e5b3069-214a-43ee-8d85-57d72000c19d.sos-ch-dk-2.exo.io",
+ "Url": "https://8-3e5b3069-214a-43ee-8d85-57d72000c19d.sos-ch-dk-2.exo.io"
+ }
diff --git a/csharp/App/SodiStoreMax/Doc/States_Table.xlsx b/csharp/App/SodiStoreMax/Doc/States_Table.xlsx
new file mode 100644
index 000000000..776df481c
Binary files /dev/null and b/csharp/App/SodiStoreMax/Doc/States_Table.xlsx differ
diff --git a/csharp/App/SodiStoreMax/Doc/TransitionToGridTied.graphml b/csharp/App/SodiStoreMax/Doc/TransitionToGridTied.graphml
new file mode 100644
index 000000000..b73645cf6
--- /dev/null
+++ b/csharp/App/SodiStoreMax/Doc/TransitionToGridTied.graphml
@@ -0,0 +1,501 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 19
+ K1 ✓
+K2 ✓
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 3
+ K1 ✓
+K2 ✓
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 9
+ K1 ✓
+K2 ✘
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ K1 ✓
+K2 ✘
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 13
+ K1 ✓
+K2 ✘
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 29
+ K1 ✓
+K2 ✘
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 23
+ K1 ✓
+K2 ✓
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+ K1 ✓
+K2 ✘
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 7
+ K1 ✓
+K2 ✓
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 11
+ K1 ✓
+K2 ✓
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 15
+ K1 ✓
+K2 ✓
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 21
+ K1 ✓
+K2 ✘
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 17
+ K1 ✓
+K2 ✘
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 25
+ K1 ✓
+K2 ✘
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 27
+ K1 ✓
+K2 ✓
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 31
+ K1 ✓
+K2 ✓
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+ turn off
+Inverters
+
+
+
+
+
+
+
+
+
+
+
+ switch to
+grid tie
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ K3's open
+
+
+
+
+
+
+
+
+
+
+
+ close K2
+
+
+
+
+
+
+
+
+
+
+
+ turn on
+Inverters
+
+
+
+
+
+
+
+
+
+
+
+ K3's close
+
+
+
+
+
+
+
+
+
+
+
+ open K2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ open K2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ turn on
+Inverters
+
+
+
+
+
+
+
+
+
+
+
+ close K2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ turn off
+inverter
+
+
+
+
+
+
+
+
+
+
+
+ turn off
+inverters
+
+
+
+
+
+
+
+
+
+
+
+ turn off
+inverters
+
+
+
+
+
+
+
+
+
+
+
+ turn off
+inverters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ turn off
+inverters
+
+
+
+
+
+
+
+
+
diff --git a/csharp/App/SodiStoreMax/Doc/TransitionToIsland.graphml b/csharp/App/SodiStoreMax/Doc/TransitionToIsland.graphml
new file mode 100644
index 000000000..800dffa96
--- /dev/null
+++ b/csharp/App/SodiStoreMax/Doc/TransitionToIsland.graphml
@@ -0,0 +1,487 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 28
+ K1 ✘
+K2 ✘
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 24
+ K1 ✘
+K2 ✘
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 8
+ K1 ✘
+K2 ✘
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 6
+ K1 ✘
+K2 ✓
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ K1 ✘
+K2 ✘
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 4
+ K1 ✘
+K2 ✘
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 22
+ K1 ✘
+K2 ✓
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 16
+ K1 ✘
+K2 ✘
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 20
+ K1 ✘
+K2 ✘
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 18
+ K1 ✘
+K2 ✓
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+ K1 ✘
+K2 ✓
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+ K1 ✘
+K2 ✓
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12
+ K1 ✘
+K2 ✘
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 14
+ K1 ✘
+K2 ✓
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 26
+ K1 ✘
+K2 ✓
+K3 ✘
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 30
+ K1 ✘
+K2 ✓
+K3 ✓
+
+
+
+
+
+
+
+
+
+
+
+
+ K3's open
+
+
+
+
+
+
+
+
+
+
+ K3's close
+
+
+
+
+
+
+
+
+
+
+ turn off
+Inverters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ switch to
+island mode
+
+
+
+
+
+
+
+
+
+
+
+ turn on
+inverters
+
+
+
+
+
+
+
+
+
+
+ turn off
+Inverters
+
+
+
+
+
+
+
+
+
+
+
+
+
+ K3 opens
+
+
+
+
+
+
+
+
+
+
+
+ open K2
+
+
+
+
+
+
+
+
+
+
+
+ turn off
+inverters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ open K2
+
+
+
+
+
+
+
+
+
+
+
+ open K2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ K3 opens
+
+
+
+
+
+
+
+
+
+
+
+ open K2
+
+
+
+
+
+
+
+
+
+
+
+ turn off
+inverters
+
+
+
+
+
+
+
+
+
+
+
+ turn off
+inverters
+
+
+
+
+
+
+
+
+
diff --git a/csharp/App/SodiStoreMax/HostList.txt b/csharp/App/SodiStoreMax/HostList.txt
new file mode 100755
index 000000000..9a03162c9
--- /dev/null
+++ b/csharp/App/SodiStoreMax/HostList.txt
@@ -0,0 +1,14 @@
+
+Prototype ie-entwicklung@10.2.3.115 Prototype
+Salimax0001 ie-entwicklung@10.2.3.104 Marti Technik (Bern)
+Salimax0002 ie-entwicklung@10.2.4.29 Weidmann d (ZG)
+Salimax0003 ie-entwicklung@10.2.4.33 Elektrotechnik Stefan GmbH
+Salimax0004 ie-entwicklung@10.2.4.32 Biohof Gubelmann (Walde)
+Salimax0004A ie-entwicklung@10.2.4.153
+Salimax0005 ie-entwicklung@10.2.4.36 Schreinerei Schönthal (Thun)
+Salimax0006 ie-entwicklung@10.2.4.35 Steakhouse Mettmenstetten
+Salimax0007 ie-entwicklung@10.2.4.154 LerchenhofHerr Twannberg
+Salimax0008 ie-entwicklung@10.2.4.113 Wittmann Kottingbrunn
+Salimax0010 ie-entwicklung@10.2.4.211 Mohatech 1 (Beat Moser)
+Salimax0011 ie-entwicklung@10.2.4.239 Thomas Tschirren (Enggistein)
+SalidomoServer ig@134.209.238.170
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/SodiStoreMax.csproj b/csharp/App/SodiStoreMax/SodiStoreMax.csproj
new file mode 100644
index 000000000..c40ce5aa8
--- /dev/null
+++ b/csharp/App/SodiStoreMax/SodiStoreMax.csproj
@@ -0,0 +1,32 @@
+
+
+
+
+ InnovEnergy.App.SodiStoreMax
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/csharp/App/SodiStoreMax/deploy.sh b/csharp/App/SodiStoreMax/deploy.sh
new file mode 100755
index 000000000..941647e33
--- /dev/null
+++ b/csharp/App/SodiStoreMax/deploy.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+dotnet_version='net6.0'
+salimax_ip="$1"
+username='ie-entwicklung'
+root_password='Salimax4x25'
+set -e
+
+echo -e "\n============================ Build ============================\n"
+
+dotnet publish \
+ ./SodiStoreMax.csproj \
+ -p:PublishTrimmed=false \
+ -c Release \
+ -r linux-x64
+
+echo -e "\n============================ Deploy ============================\n"
+
+rsync -v \
+ --exclude '*.pdb' \
+ ./bin/Release/$dotnet_version/linux-x64/publish/* \
+ $username@"$salimax_ip":~/salimax
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/deploy_all_installations.sh b/csharp/App/SodiStoreMax/deploy_all_installations.sh
new file mode 100755
index 000000000..533946856
--- /dev/null
+++ b/csharp/App/SodiStoreMax/deploy_all_installations.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+dotnet_version='net6.0'
+salimax_ip="$1"
+username='ie-entwicklung'
+root_password='Salimax4x25'
+
+set -e
+
+echo -e "\n============================ Build ============================\n"
+
+dotnet publish \
+ ./SaliMax.csproj \
+ -p:PublishTrimmed=false \
+ -c Release \
+ -r linux-x64
+
+echo -e "\n============================ Deploy ============================\n"
+#ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.29" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.211")
+#ip_addresses=("10.2.4.154" "10.2.4.29")
+ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.29" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" )
+
+
+
+for ip_address in "${ip_addresses[@]}"; do
+ rsync -v \
+ --exclude '*.pdb' \
+ ./bin/Release/$dotnet_version/linux-x64/publish/* \
+ $username@"$ip_address":~/salimax
+
+ ssh "$username"@"$ip_address" "cd salimax && echo '$root_password' | sudo -S ./restart"
+
+ echo "Deployed and ran commands on $ip_address"
+done
+
+
+
diff --git a/csharp/App/SodiStoreMax/downloadBatteryLogs/download-bms-log b/csharp/App/SodiStoreMax/downloadBatteryLogs/download-bms-log
new file mode 100644
index 000000000..b9c2e8f23
--- /dev/null
+++ b/csharp/App/SodiStoreMax/downloadBatteryLogs/download-bms-log
@@ -0,0 +1,284 @@
+#!/usr/bin/python2 -u
+# coding=utf-8
+import os
+import re
+import struct
+import serial
+import logging
+from sys import argv, exit
+from datetime import datetime
+from pymodbus.pdu import ModbusRequest, ModbusResponse, ExceptionResponse
+from pymodbus.other_message import ReportSlaveIdRequest
+from pymodbus.exceptions import ModbusException
+from pymodbus.pdu import ExceptionResponse
+from pymodbus.factory import ClientDecoder
+from pymodbus.client import ModbusSerialClient as Modbus
+logging.basicConfig(level=logging.INFO)
+
+
+
+# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
+# noinspection PyUnreachableCode
+if False:
+ from typing import List, Optional, NoReturn
+
+RESET_REGISTER = 0x2087
+FIRMWARE_VERSION_REGISTER = 1054
+SERIAL_STARTER_DIR = '/opt/victronenergy/serial-starter/'
+INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
+OUTPUT_DIR = '/data/innovenergy'
+
+
+class ReadLogRequest(ModbusRequest):
+
+ function_code = 0x42
+ _rtu_frame_size = 5 # not used
+
+ def __init__(self, address = None, **kwargs):
+
+ ModbusRequest.__init__(self, **kwargs)
+ self.sub_function = 0 if address is None else 1
+ self.address = address
+
+ # FUGLY as hell, but necessary bcs PyModbus cannot deal
+ # with responses that have lengths depending on the sub_function.
+ # it goes without saying that this isn't thread-safe
+ ReadLogResponse._rtu_frame_size = 9 if self.sub_function == 0 else 9+128
+
+ def encode(self):
+
+ if self.sub_function == 0:
+ return struct.pack('>B', self.sub_function)
+ else:
+ return struct.pack('>BI', self.sub_function, self.address)
+
+ def decode(self, data):
+ self.sub_function = struct.unpack('>B', data)
+
+ def execute(self, context):
+ print("EXECUTE1")
+
+ def get_response_pdu_size(self):
+ return ReadLogResponse._rtu_frame_size - 3
+
+ def __str__(self):
+ return "ReadLogAddressRequest"
+
+
+class ReadLogResponse(ModbusResponse):
+
+ function_code = 0x42
+ _rtu_frame_size = 9 # the WHOLE frame incl crc
+
+ def __init__(self, sub_function=0, address=b'\x00', data=None, **kwargs):
+ ModbusResponse.__init__(self, **kwargs)
+ self.sub_function = sub_function
+ self.address = address
+ self.data = data
+
+ def encode(self):
+ pass
+
+ def decode(self, data):
+ self.address, self.address = struct.unpack_from(">BI", data)
+ self.data = data[5:]
+
+ def __str__(self):
+ arguments = (self.function_code, self.address)
+ return "ReadLogAddressResponse(%s, %s)" % arguments
+
+# unfortunately we have to monkey-patch this global table because
+# the current (victron) version of PyModbus does not have a
+# way to "register" new function-codes yet
+ClientDecoder.function_table.append(ReadLogResponse)
+
+
+class LockTTY(object):
+
+ def __init__(self, tty):
+ # type: (str) -> None
+ self.tty = tty
+
+ def __enter__(self):
+ os.system(SERIAL_STARTER_DIR + 'stop-tty.sh ' + self.tty)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ os.system(SERIAL_STARTER_DIR + 'start-tty.sh ' + self.tty)
+
+
+def wrap_try_except(error_msg):
+ def decorate(f):
+ def applicator(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except:
+ print(error_msg)
+ exit(1)
+ return applicator
+ return decorate
+
+
+def init_modbus(tty):
+ # type: (str) -> Modbus
+
+ return Modbus(
+ port='/dev/' + tty,
+ method='rtu',
+ baudrate=115200,
+ stopbits=1,
+ bytesize=8,
+ timeout=0.5, # seconds
+ parity=serial.PARITY_ODD)
+
+
+@wrap_try_except("Failed to download BMS log!")
+def download_log(modbus, node_id, battery_id):
+ # type: (Modbus, int, str) -> NoReturn
+
+ # Get address of latest log entry
+ # request = ReadLogRequest(unit=slave_id)
+
+ print ('downloading BMS log from node ' + str(node_id) + ' ...')
+
+ progress = -1
+ log_file = battery_id + "-node" + str(node_id) + "-" + datetime.now().strftime('%d-%m-%Y') + ".bin"
+ print(log_file)
+
+ with open(log_file, 'w') as f:
+
+ eof = 0x200000
+ record = 0x40
+ for address in range(0, eof, 2*record):
+
+ percent = int(100*address/eof)
+
+ if percent != progress:
+ progress = percent
+ print('\r{}% '.format(progress),end='')
+
+ request = ReadLogRequest(address, slave=node_id)
+ result = modbus.execute(request) # type: ReadLogResponse
+
+ address1 = "{:06X}".format(address)
+ address2 = "{:06X}".format(address+record)
+
+ data1 = result.data[:record]
+ data2 = result.data[record:]
+
+ line1 = address1 + ":" + ''.join('{:02X}'.format(byte) for byte in data1)
+ line2 = address2 + ":" + ''.join('{:02X}'.format(byte) for byte in data2)
+
+ lines = line1 + "\n" + line2 + "\n"
+ f.write(lines)
+
+ print("\r100%")
+ print("done")
+ print("wrote log to " + log_file)
+
+ return True
+
+
+@wrap_try_except("Failed to contact battery!")
+def identify_battery(modbus, node_id):
+ # type: (Modbus, int) -> str
+
+ target = 'battery #' + str(node_id)
+ print('contacting ' + target + ' ...')
+
+ request = ReportSlaveIdRequest(slave=node_id)
+ response = modbus.execute(request)
+
+ index_of_ff = response.identifier.find(b'\xff')
+ sid_response = response.identifier[index_of_ff + 1:].decode('utf-8').split(' ')
+
+ response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, slave=node_id)
+
+ fw = '{0:0>4X}'.format(response.registers[0])
+ print("log string is",sid_response[0]+"-"+sid_response[1]+"-"+fw)
+
+ #return re.sub(" +", "-", sid + " " + fw)
+ return sid_response[0]+"-"+sid_response[1]+"-"+fw
+
+
+def is_int(value):
+ # type: (str) -> bool
+ try:
+ _ = int(value)
+ return True
+ except ValueError:
+ return False
+
+
+def print_usage():
+ print ('Usage: ' + __file__ + ' ')
+ print ('Example: ' + __file__ + ' 2 ttyUSB0')
+ print ('')
+ print ('You can omit the "ttyUSB" prefix of the serial device:')
+ print (' ' + __file__ + ' 2 0')
+ print ('')
+ print ('You can omit the serial device entirely when the "com.victronenergy.battery." service is running:')
+ print (' ' + __file__ + ' 2')
+ print ('')
+
+
+def get_tty_from_battery_service_name():
+ # type: () -> Optional[str]
+
+ import dbus
+ bus = dbus.SystemBus()
+
+ tty = (
+ name.split('.')[-1]
+ for name in bus.list_names()
+ if name.startswith('com.victronenergy.battery.')
+ )
+
+ return next(tty, None)
+
+
+def parse_tty(tty):
+ # type: (Optional[str]) -> str
+
+ if tty is None:
+ return get_tty_from_battery_service_name()
+
+ if is_int(tty):
+ return 'ttyUSB' + argv[1]
+ else:
+ return tty
+
+
+def parse_cmdline_args(argv):
+ # type: (List[str]) -> (str, int)
+
+ slave_id = element_at_or_none(argv, 0)
+ tty = parse_tty(element_at_or_none(argv, 1))
+
+ if slave_id is None or tty is None:
+ print_usage()
+ exit(2)
+
+ print("tty=",tty)
+ print("slave id= ",slave_id)
+
+ return tty, int(slave_id)
+
+
+def element_at_or_none(lst, index):
+ return next(iter(lst[index:]), None)
+
+
+def main(argv):
+ # type: (List[str]) -> ()
+
+ tty, node_id = parse_cmdline_args(argv)
+
+ with init_modbus(tty) as modbus:
+ battery_id = identify_battery(modbus, node_id)
+ download_log(modbus, node_id, battery_id)
+
+ exit(0)
+
+
+main(argv[1:])
diff --git a/csharp/App/SodiStoreMax/downloadBatteryLogs/download_battery_logs.sh b/csharp/App/SodiStoreMax/downloadBatteryLogs/download_battery_logs.sh
new file mode 100755
index 000000000..20c3f05b4
--- /dev/null
+++ b/csharp/App/SodiStoreMax/downloadBatteryLogs/download_battery_logs.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+#Prototype 10.2.3.115 Prototype
+#Salimax0001 10.2.3.104 Marti Technik (Bern)
+#Salimax0002 10.2.4.29 Weidmann d (ZG)
+#Salimax0003 10.2.4.33 Elektrotechnik Stefan GmbH
+#Salimax0004 10.2.4.32 Biohof Gubelmann (Walde)
+#Salimax0005 10.2.4.36 Schreinerei Schönthal (Thun)
+#Salimax0006 10.2.4.35 Steakhouse Mettmenstetten
+#Salimax0007 10.2.4.154 LerchenhofHerr Twannberg
+#Salimax0008 10.2.4.113 Wittmann Kottingbrunn
+
+dotnet_version='net6.0'
+ip_address="$1"
+battery_ids="$2"
+username='ie-entwicklung'
+root_password='Salimax4x25'
+
+if [ "$#" -lt 2 ]; then
+ echo "Error: Insufficient arguments. Usage: $0 "
+ exit 1
+fi
+
+# Function to expand battery ids from a range
+expand_battery_ids() {
+ local range="$1"
+ local expanded_ids=()
+
+ IFS='-' read -r start end <<< "$range"
+ for ((i = start; i <= end; i++)); do
+ expanded_ids+=("$i")
+ done
+
+ echo "${expanded_ids[@]}"
+}
+
+# Check if battery_ids_arg contains a hyphen indicating a range
+if [[ "$battery_ids" == *-* ]]; then
+ # Expand battery ids from the range
+ battery_ids=$(expand_battery_ids "$battery_ids")
+else
+ # Use the provided battery ids
+ battery_ids=("$battery_ids")
+fi
+
+echo "ip_address: $ip_address"
+echo "Battery_ids: ${battery_ids[@]}"
+
+#ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.29")
+#battery_ids=("2" "3" "4" "5" "6" "7" "8" "9" "10" "11")
+
+set -e
+
+scp download-bms-log "$username"@"$ip_address":/home/"$username"
+ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl stop battery.service"
+ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S apt install python3-pip -y"
+ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S pip3 install pymodbus"
+
+for battery in "${battery_ids[@]}"; do
+ ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S python3 download-bms-log " "$battery" " ttyUSB0"
+done
+ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl start battery.service"
+ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S rm download-bms-log"
+scp "$username"@"$ip_address":/home/"$username/*.bin" .
+ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S rm *.bin"
+
+echo "Deployed and ran commands on $ip_address"
+done
+
+
diff --git a/csharp/App/SodiStoreMax/resources/PublicKey b/csharp/App/SodiStoreMax/resources/PublicKey
new file mode 100644
index 000000000..ae41b2935
--- /dev/null
+++ b/csharp/App/SodiStoreMax/resources/PublicKey
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCed5ANekhbdV/8nEwFyaqxbPGON+NZKAkZXKx2aMAbX6jYQpusXSf4lKxEp4vHX9q2ScWycluUEhlzwe9vaWIK6mxEG9gjtU0/tKIavqZ6qpcuiglal750e8tlDh+lAgg5K3v4tvV4uVEfFc42UzSC9cIBBKPBC41dc0xQKyFIDsSH6Qha1nyncKRC3OXUkOiiRvmbd4PVc9A5ah2vt+661pghZE19Qeh5ROn/Sma9C+9QIyUDCylezqptnT+Jdvs+JMCHk8nKK2A0bz1w0a8zzO7M1RLHfBLQ6o1SQAdV/Pmon8uQ9vLHc86l5r7WSTMEcjAqY3lGE9mdxsSZWNmp InnovEnergy
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/resources/Salimax.Service b/csharp/App/SodiStoreMax/resources/Salimax.Service
new file mode 100644
index 000000000..d823b591f
--- /dev/null
+++ b/csharp/App/SodiStoreMax/resources/Salimax.Service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Salimax Controller
+Wants=battery.service
+
+[Service]
+WorkingDirectory=/home/ie-entwicklung/salimax
+ExecStart=/home/ie-entwicklung/salimax/SaliMax
+WatchdogSec=30s
+Restart=always
+RestartSec=500ms
+
+[Install]
+WantedBy=multi-user.target
diff --git a/csharp/App/SodiStoreMax/src/AggregationService/Aggregator.cs b/csharp/App/SodiStoreMax/src/AggregationService/Aggregator.cs
new file mode 100644
index 000000000..0fa03f49e
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/AggregationService/Aggregator.cs
@@ -0,0 +1,381 @@
+using InnovEnergy.App.SodiStoreMax.Ess;
+using InnovEnergy.Lib.Utils;
+using static System.Double;
+
+namespace InnovEnergy.App.SodiStoreMax.AggregationService;
+
+public static class Aggregator
+{
+
+ public static async Task HourlyDataAggregationManager()
+ {
+ var currentDateTime = DateTime.Now;
+ var nextRoundedHour = currentDateTime.AddHours(1).AddMinutes(-currentDateTime.Minute).AddSeconds(-currentDateTime.Second);
+
+ // Calculate the time until the next rounded hour
+ var timeUntilNextHour = nextRoundedHour - currentDateTime;
+
+ // Output the current and next rounded hour times
+ Console.WriteLine("------------------------------------------HourlyDataAggregationManager-------------------------------------------");
+ Console.WriteLine("Current Date and Time: " + currentDateTime);
+ Console.WriteLine("Next Rounded Hour: " + nextRoundedHour);
+ // Output the time until the next rounded hour
+ Console.WriteLine("Waiting for " + timeUntilNextHour.TotalMinutes + " minutes...");
+ Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
+
+ // Wait until the next rounded hour
+ await Task.Delay(timeUntilNextHour);
+
+ while (true)
+ {
+ try
+ {
+ AggregatedData hourlyAggregatedData = CreateHourlyData("LogDirectory",DateTime.Now.AddHours(-1).ToUnixTime(),DateTime.Now.ToUnixTime());
+ hourlyAggregatedData.Save("HourlyData");
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine("An error has occured when calculating hourly aggregated data, exception is:\n" + e);
+ }
+ await Task.Delay(TimeSpan.FromHours(1));
+ }
+ }
+
+ public static async Task DailyDataAggregationManager()
+ {
+ var currentDateTime = DateTime.Now;
+ var nextRoundedHour = currentDateTime.AddDays(1).AddHours(-currentDateTime.Hour).AddMinutes(-currentDateTime.Minute).AddSeconds(-currentDateTime.Second);
+
+ // Calculate the time until the next rounded hour
+ var timeUntilNextDay = nextRoundedHour - currentDateTime;
+ Console.WriteLine("------------------------------------------DailyDataAggregationManager-------------------------------------------");
+ // Output the current and next rounded hour times
+ Console.WriteLine("Current Date and Time: " + currentDateTime);
+ Console.WriteLine("Next Rounded Hour: " + nextRoundedHour);
+ // Output the time until the next rounded hour
+ Console.WriteLine("Waiting for " + timeUntilNextDay.TotalHours + " hours...");
+ Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
+
+ // Wait until the next rounded hour
+ await Task.Delay(timeUntilNextDay);
+
+ while (true)
+ {
+ try
+ {
+ var currentTime = DateTime.Now;
+ AggregatedData dailyAggregatedData = CreateDailyData("HourlyData",currentTime.AddDays(-1).ToUnixTime(),currentTime.ToUnixTime());
+ dailyAggregatedData.Save("DailyData");
+ if (await dailyAggregatedData.PushToS3())
+ {
+ //DeleteHourlyData("HourlyData",currentTime.ToUnixTime());
+ //AggregatedData.DeleteDailyData("DailyData");
+ }
+
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine("An error has occured when calculating daily aggregated data, exception is:\n" + e);
+ }
+ await Task.Delay(TimeSpan.FromDays(1));
+ }
+ }
+
+ private static void DeleteHourlyData(String myDirectory, Int64 beforeTimestamp)
+ {
+ var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
+ Console.WriteLine("Delete data before"+beforeTimestamp);
+ foreach (var csvFile in csvFiles)
+ {
+ if (IsFileWithinTimeRange(csvFile, 0, beforeTimestamp))
+ {
+ File.Delete(csvFile);
+ Console.WriteLine($"Deleted hourly data file: {csvFile}");
+ }
+ }
+ }
+
+ // this for test
+ private static AggregatedData CreateHourlyData(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp)
+ {
+ // Get all CSV files in the specified directory
+ var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
+ var batterySoc = new List();
+ var pvPowerSum = new List();
+ var heatingPower = new List();
+ var gridPowerImport = new List();
+ var gridPowerExport = new List();
+ var batteryDischargePower = new List();
+ var batteryChargePower = new List();
+
+
+ Console.WriteLine("File timestamp should start after "+ afterTimestamp);
+
+ foreach (var csvFile in csvFiles)
+ {
+ if (csvFile == "LogDirectory/log.csv")
+ {
+ continue;
+ }
+
+ if (IsFileWithinTimeRange(csvFile, afterTimestamp, beforeTimestamp))
+ {
+ using var reader = new StreamReader(csvFile);
+
+ while (!reader.EndOfStream)
+ {
+
+ var line = reader.ReadLine();
+ var lines = line?.Split(';');
+
+ // Assuming there are always three columns (variable name and its value)
+ if (lines is { Length: 3 })
+ {
+ var variableName = lines[0].Trim();
+
+ if (TryParse(lines[1].Trim(), out var value))
+ {
+ switch (variableName)
+ {
+ case "/Battery/Soc":
+ batterySoc.Add(value);
+ break;
+
+ case "/PvOnDc/DcWh" :
+ pvPowerSum.Add(value);
+ break;
+
+ case "/Battery/Dc/Power":
+
+ if (value < 0)
+ {
+ batteryDischargePower.Add(value);
+ }
+ else
+ {
+ batteryChargePower.Add(value);
+
+ }
+ break;
+
+ case "/GridMeter/ActivePowerExportT3":
+ // we are using different register to check which value from the grid meter we need to use
+ // At the moment register 8002 amd 8012. in KWh
+ gridPowerExport.Add(value);
+ break;
+ case "/GridMeter/ActivePowerImportT3":
+ gridPowerImport.Add(value);
+ break;
+ case "/Battery/HeatingPower":
+ heatingPower.Add(value);
+ break;
+ // Add more cases as needed
+ default:
+ // Code to execute when variableName doesn't match any condition
+ break;
+ }
+
+ }
+ else
+ {
+ //Handle cases where variableValue is not a valid number
+ // Console.WriteLine(
+ // $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
+ }
+ }
+ else
+ {
+ // Handle invalid column format
+ //Console.WriteLine("Invalid format in column");
+ }
+ }
+ }
+ }
+
+ //Average Power (Watts)= Sum of Power Readings/Number of Readings
+
+ //Then, you can use the average power in the energy formula:
+ //
+ //Energy (kWh)= (Average Power / 3600) × Time (1 seconds)
+ //
+ // Dividing the Average power readings by 3600 converts the result from watt-seconds to kilowatt-hours.
+
+ var dischargingEnergy = (batteryDischargePower.Any() ? batteryDischargePower.Average() : 0.0) / 3600;
+ var chargingEnergy = (batteryChargePower.Any() ? batteryChargePower.Average() : 0.0) / 3600;
+ var heatingPowerAvg = (heatingPower.Any() ? heatingPower.Average() : 0.0) / 3600;
+
+ var dMaxSoc = batterySoc.Any() ? batterySoc.Max() : 0.0;
+ var dMinSoc = batterySoc.Any() ? batterySoc.Min() : 0.0;
+ var dSumGridExportPower = gridPowerExport.Any() ? gridPowerExport.Max() - gridPowerExport.Min(): 0.0;
+ var dSumGridImportPower = gridPowerImport.Any() ? gridPowerImport.Max() - gridPowerImport.Min(): 0.0;
+ var dSumPvPower = pvPowerSum.Any() ? pvPowerSum.Max() : 0.0;
+
+
+ AggregatedData aggregatedData = new AggregatedData
+ {
+ MaxSoc = dMaxSoc,
+ MinSoc = dMinSoc,
+ DischargingBatteryPower = dischargingEnergy,
+ ChargingBatteryPower = chargingEnergy,
+ GridExportPower = dSumGridExportPower,
+ GridImportPower = dSumGridImportPower,
+ PvPower = dSumPvPower,
+ HeatingPower = heatingPowerAvg
+ };
+
+ // Print the stored CSV data for verification
+ Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}");
+ Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
+
+ Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.DischargingBatteryPower}");
+ Console.WriteLine($"ChargingBatteryPower: {aggregatedData.ChargingBatteryPower}");
+
+ Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}");
+ Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}");
+
+ Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
+
+
+ Console.WriteLine("CSV data reading and storage completed.");
+ Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
+
+ return aggregatedData;
+ }
+
+ private static AggregatedData CreateDailyData(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp)
+ {
+ // Get all CSV files in the specified directory
+ var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
+ var batterySoc = new List();
+ var pvPower = new List();
+ var gridPowerImport = new List();
+ var gridPowerExport = new List();
+ var batteryDischargePower = new List();
+ var batteryChargePower = new List();
+ var heatingPowerAvg = new List();
+
+
+
+ Console.WriteLine("File timestamp should start after "+ afterTimestamp);
+
+ foreach (var csvFile in csvFiles)
+ {
+ if (csvFile == "LogDirectory/log.csv")
+ {
+ continue;
+ }
+
+ if (IsFileWithinTimeRange(csvFile, afterTimestamp, beforeTimestamp))
+ {
+ using var reader = new StreamReader(csvFile);
+
+ while (!reader.EndOfStream)
+ {
+
+ var line = reader.ReadLine();
+ var lines = line?.Split(';');
+
+ // Assuming there are always three columns (variable name and its value)
+ if (lines is { Length: 3 })
+ {
+ var variableName = lines[0].Trim();
+
+ if (TryParse(lines[1].Trim(), out var value))
+ {
+ switch (variableName)
+ {
+ case "/MinSoc" or "/MaxSoc":
+ batterySoc.Add(value);
+ break;
+
+ case "/PvPower":
+ pvPower.Add(value);
+ break;
+
+ case "/DischargingBatteryPower" :
+ batteryDischargePower.Add(value);
+ break;
+
+ case "/ChargingBatteryPower" :
+ batteryChargePower.Add(value);
+ break;
+
+ case "/GridExportPower":
+ gridPowerExport.Add(value);
+ break;
+
+ case "/GridImportPower":
+ gridPowerImport.Add(value);
+ break;
+
+ case "/HeatingPower":
+ heatingPowerAvg.Add(value);
+ break;
+ // Add more cases as needed
+ default:
+ // Code to execute when variableName doesn't match any condition
+ break;
+ }
+
+ }
+ else
+ {
+ //Handle cases where variableValue is not a valid number
+ // Console.WriteLine(
+ // $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
+ }
+ }
+ else
+ {
+ // Handle invalid column format
+ //Console.WriteLine("Invalid format in column");
+ }
+ }
+ }
+ }
+
+ AggregatedData aggregatedData = new AggregatedData
+ {
+ MaxSoc = batterySoc.Any() ? batterySoc.Max() : 0.0,
+ MinSoc = batterySoc.Any() ? batterySoc.Min() : 0.0,
+ DischargingBatteryPower = batteryDischargePower.Any() ? batteryDischargePower.Average(): 0.0,
+ ChargingBatteryPower = batteryChargePower.Any() ? batteryChargePower.Average() : 0.0,
+ GridExportPower = gridPowerExport.Any() ? gridPowerExport.Sum() : 0.0,
+ GridImportPower = gridPowerImport.Any() ? gridPowerImport.Sum() : 0.0,
+ PvPower = pvPower.Any() ? pvPower.Last() : 0.0,
+ HeatingPower = heatingPowerAvg.Any() ? heatingPowerAvg.Average() : 0.0,
+ };
+
+ // Print the stored CSV data for verification
+ Console.WriteLine($"Pv Power: {aggregatedData.PvPower}");
+ Console.WriteLine($"Heating Power: {aggregatedData.HeatingPower}");
+ Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}");
+ Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
+
+ Console.WriteLine($"ChargingBatteryPower: {aggregatedData.DischargingBatteryPower}");
+ Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.ChargingBatteryPower}");
+
+ Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}");
+ Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}");
+
+
+
+ Console.WriteLine("CSV data reading and storage completed.");
+ Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
+
+ return aggregatedData;
+ }
+
+ // Custom method to check if a string is numeric
+ private static Boolean GetVariable(String value, String path)
+ {
+ return value == path;
+ }
+
+ private static Boolean IsFileWithinTimeRange(string filePath, long startTime, long endTime)
+ {
+ var fileTimestamp = long.TryParse(Path.GetFileNameWithoutExtension(filePath).Replace("log_", ""), out var fileTimestamp1) ? fileTimestamp1 : -1;
+
+ return fileTimestamp >= startTime && fileTimestamp < endTime;
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/AggregationService/HourlyData.cs b/csharp/App/SodiStoreMax/src/AggregationService/HourlyData.cs
new file mode 100644
index 000000000..a6294a20b
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/AggregationService/HourlyData.cs
@@ -0,0 +1,130 @@
+using System.IO.Compression;
+using System.Text;
+using System.Text.Json;
+using Flurl.Http;
+using InnovEnergy.App.SodiStoreMax.Devices;
+using InnovEnergy.App.SodiStoreMax.SystemConfig;
+using InnovEnergy.Lib.Units;
+using InnovEnergy.Lib.Utils;
+using static System.Text.Json.JsonSerializer;
+
+namespace InnovEnergy.App.SodiStoreMax.AggregationService;
+// shut up trim warnings
+#pragma warning disable IL2026
+
+public class AggregatedData
+{
+ public required Double MinSoc { get; set; }
+ public required Double MaxSoc { get; set; }
+ public required Double PvPower { get; set; }
+ public required Double DischargingBatteryPower { get; set; }
+ public required Double ChargingBatteryPower { get; set; }
+ public required Double GridExportPower { get; set; }
+ public required Double GridImportPower { get; set; }
+ public required Double HeatingPower { get; set; }
+
+
+ private readonly S3Config? _S3Config = Config.Load().S3;
+
+ public void Save(String directory)
+ {
+ var date = DateTime.Now.ToUnixTime();
+ var defaultHDataPath = Environment.CurrentDirectory + "/" + directory + "/";
+ var dataFilePath = defaultHDataPath + date + ".csv";
+
+ if (!Directory.Exists(defaultHDataPath))
+ {
+ Directory.CreateDirectory(defaultHDataPath);
+ Console.WriteLine("Directory created successfully.");
+ }
+ Console.WriteLine("data file path is " + dataFilePath);
+
+ try
+ {
+ var csvString = this.ToCsv();
+ File.WriteAllText(dataFilePath, csvString);
+ }
+ catch (Exception e)
+ {
+ $"Failed to write config file {dataFilePath}\n{e}".WriteLine();
+ throw;
+ }
+ }
+
+ public static void DeleteDailyData(String directory)
+ {
+
+ var csvFiles = Directory.GetFiles(directory, "*.csv");
+ foreach (var csvFile in csvFiles)
+ {
+ File.Delete(csvFile);
+ Console.WriteLine($"Deleted daily data file: {csvFile}");
+ }
+ }
+
+ public async Task PushToS3()
+ {
+ var csv = this.ToCsv();
+ if (_S3Config is null)
+ return false;
+
+ var s3Path = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd") + ".csv";
+ var request = _S3Config.CreatePutRequest(s3Path);
+
+ // Compress CSV data to a byte array
+ byte[] compressedBytes;
+ using (var memoryStream = new MemoryStream())
+ {
+ //Create a zip directory and put the compressed file inside
+ using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
+ {
+ var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
+ using (var entryStream = entry.Open())
+ using (var writer = new StreamWriter(entryStream))
+ {
+ writer.Write(csv);
+ }
+ }
+
+ compressedBytes = memoryStream.ToArray();
+ }
+
+ // Encode the compressed byte array as a Base64 string
+ string base64String = Convert.ToBase64String(compressedBytes);
+
+ // Create StringContent from Base64 string
+ var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
+
+ // Upload the compressed data (ZIP archive) to S3
+ var response = await request.PutAsync(stringContent);
+
+ //
+ // var request = _S3Config.CreatePutRequest(s3Path);
+ // var response = await request.PutAsync(new StringContent(csv));
+
+ if (response.StatusCode != 200)
+ {
+ Console.WriteLine("ERROR: PUT");
+ var error = await response.GetStringAsync();
+ Console.WriteLine(error);
+ return false;
+ }
+
+ return true;
+ }
+
+ // public static HourlyData? Load(String dataFilePath)
+ // {
+ // try
+ // {
+ // var csvString = File.ReadAllText(dataFilePath);
+ // return Deserialize(jsonString)!;
+ // }
+ // catch (Exception e)
+ // {
+ // $"Failed to read config file {dataFilePath}, using default config\n{e}".WriteLine();
+ // return null;
+ // }
+ // }
+
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/DataTypes/AlarmOrWarning.cs b/csharp/App/SodiStoreMax/src/DataTypes/AlarmOrWarning.cs
new file mode 100644
index 000000000..762caad37
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/DataTypes/AlarmOrWarning.cs
@@ -0,0 +1,9 @@
+namespace InnovEnergy.App.SodiStoreMax.DataTypes;
+
+public class AlarmOrWarning
+{
+ public String? Date { get; set; }
+ public String? Time { get; set; }
+ public String? Description { get; set; }
+ public String? CreatedBy { get; set; }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/DataTypes/Configuration.cs b/csharp/App/SodiStoreMax/src/DataTypes/Configuration.cs
new file mode 100644
index 000000000..8610e2601
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/DataTypes/Configuration.cs
@@ -0,0 +1,12 @@
+using InnovEnergy.App.SodiStoreMax.SystemConfig;
+
+namespace InnovEnergy.App.SodiStoreMax.DataTypes;
+
+public class Configuration
+{
+ public Double MinimumSoC { get; set; }
+ public Double GridSetPoint { get; set; }
+ public CalibrationChargeType CalibrationChargeState { get; set; }
+ public DateTime CalibrationChargeDate { get; set; }
+}
+
diff --git a/csharp/App/SodiStoreMax/src/DataTypes/StatusMessage.cs b/csharp/App/SodiStoreMax/src/DataTypes/StatusMessage.cs
new file mode 100644
index 000000000..d54d1f586
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/DataTypes/StatusMessage.cs
@@ -0,0 +1,20 @@
+using InnovEnergy.App.SodiStoreMax.Ess;
+
+namespace InnovEnergy.App.SodiStoreMax.DataTypes;
+
+public class StatusMessage
+{
+ public required Int32 InstallationId { get; set; }
+ public required Int32 Product { get; set; }
+ public required SalimaxAlarmState Status { get; set; }
+ public required MessageType Type { get; set; }
+ public List? Warnings { get; set; }
+ public List? Alarms { get; set; }
+ public Int32 Timestamp { get; set; }
+}
+
+public enum MessageType
+{
+ AlarmOrWarning,
+ Heartbit
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Devices/AcPowerDevice.cs b/csharp/App/SodiStoreMax/src/Devices/AcPowerDevice.cs
new file mode 100644
index 000000000..0b1033a05
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Devices/AcPowerDevice.cs
@@ -0,0 +1,8 @@
+using InnovEnergy.Lib.Units.Composite;
+
+namespace InnovEnergy.App.SodiStoreMax.Devices;
+
+public class AcPowerDevice
+{
+ public required AcPower Power { get; init; }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Devices/DcPowerDevice.cs b/csharp/App/SodiStoreMax/src/Devices/DcPowerDevice.cs
new file mode 100644
index 000000000..5579c5f12
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Devices/DcPowerDevice.cs
@@ -0,0 +1,8 @@
+using InnovEnergy.Lib.Units.Power;
+
+namespace InnovEnergy.App.SodiStoreMax.Devices;
+
+public class DcPowerDevice
+{
+ public required DcPower Power { get; init; }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Devices/DeviceState.cs b/csharp/App/SodiStoreMax/src/Devices/DeviceState.cs
new file mode 100644
index 000000000..6814e10ec
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Devices/DeviceState.cs
@@ -0,0 +1,8 @@
+namespace InnovEnergy.App.SodiStoreMax.Devices;
+
+public enum DeviceState
+{
+ Disabled,
+ Measured,
+ Computed
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Devices/SalimaxDevice.cs b/csharp/App/SodiStoreMax/src/Devices/SalimaxDevice.cs
new file mode 100644
index 000000000..61281bb24
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Devices/SalimaxDevice.cs
@@ -0,0 +1,8 @@
+using InnovEnergy.Lib.Utils.Net;
+
+namespace InnovEnergy.App.SodiStoreMax.Devices;
+
+public class SalimaxDevice : Ip4Address
+{
+ public required DeviceState DeviceState { get; init; }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Ess/Controller.cs b/csharp/App/SodiStoreMax/src/Ess/Controller.cs
new file mode 100644
index 000000000..ff8daea6d
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Ess/Controller.cs
@@ -0,0 +1,273 @@
+using InnovEnergy.App.SodiStoreMax.SystemConfig;
+using InnovEnergy.Lib.Devices.BatteryDeligreen;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
+using InnovEnergy.Lib.Units;
+using InnovEnergy.Lib.Utils;
+
+namespace InnovEnergy.App.SodiStoreMax.Ess;
+
+public static class Controller
+{
+ private static readonly Double MaxDischargePower = -4000; // By battery TODO: move to config
+ private static readonly Double MaxChargePower = 3500; // By battery TODO: move to config
+
+ public static EssMode SelectControlMode(this StatusRecord s)
+ {
+ //return EssMode.OptimizeSelfConsumption;
+
+ return s.StateMachine.State != 23 ? EssMode.Off
+ : s.MustReachMinSoc() ? EssMode.ReachMinSoc
+ : s.GridMeter is null ? EssMode.NoGridMeter
+ : EssMode.OptimizeSelfConsumption;
+ }
+
+
+ public static EssControl ControlEss(this StatusRecord s)
+ {
+ var mode = s.SelectControlMode().WriteLine();
+
+ if (mode is EssMode.Off) // to test on prototype
+ {
+ if (s.StateMachine.State == 28 )
+ {
+ return new EssControl
+ {
+ LimitedBy = EssLimit.NoLimit,
+ Mode = EssMode.OffGrid,
+ PowerCorrection = 0,
+ PowerSetpoint = 0
+ };
+ }
+ return EssControl.Default;
+ }
+
+ // if we have no reading from the Grid meter, but we have a grid power (K1 is close),
+ // then we do only heat the battery to avoid discharging the battery and the oscillation between reach min soc and off mode
+ if (mode is EssMode.NoGridMeter)
+ return new EssControl
+ {
+ LimitedBy = EssLimit.NoLimit,
+ Mode = EssMode.NoGridMeter,
+ PowerCorrection = 0,
+ PowerSetpoint = 0, //s.Battery == null ? 1000 : s.Battery.Devices.Count * s.Config.BatterySelfDischargePower // 1000 default value for heating the battery
+ };
+
+ var essDelta = s.ComputePowerDelta(mode);
+ essDelta.WriteLine("Power Correction");
+
+ var unlimitedControl = new EssControl
+ {
+ Mode = mode,
+ LimitedBy = EssLimit.NoLimit,
+ PowerCorrection = essDelta,
+ PowerSetpoint = 0
+ };
+
+ var limitedControl = unlimitedControl
+ .LimitChargePower(s)
+ .LimitDischargePower(s)
+ .LimitInverterPower(s);
+
+ var currentPowerSetPoint = s.CurrentPowerSetPoint();
+
+ return limitedControl with { PowerSetpoint = currentPowerSetPoint + limitedControl.PowerCorrection };
+ }
+
+ private static EssControl LimitInverterPower(this EssControl control, StatusRecord s)
+ {
+ var powerDelta = control.PowerCorrection.Value;
+
+ var acDcs = s.AcDc.Devices;
+
+ var nInverters = acDcs.Count;
+
+ if (nInverters < 2)
+ return control; // current loop cannot happen
+
+ var nominalPower = acDcs.Average(d => d.Status.Nominal.Power);
+ var maxStep = nominalPower / 25; //TODO magic number to config
+
+ var clampedPowerDelta = powerDelta.Clamp(-maxStep, maxStep);
+
+ var dcLimited = acDcs.Any(d => d.Status.PowerLimitedBy == PowerLimit.DcLink);
+
+ if (!dcLimited)
+ return control with { PowerCorrection = clampedPowerDelta };
+
+ var maxPower = acDcs.Max(d => d.Status.Ac.Power.Active.Value);
+ var minPower = acDcs.Min(d => d.Status.Ac.Power.Active.Value);
+
+ var powerDifference = maxPower - minPower;
+
+ if (powerDifference < maxStep)
+ return control with { PowerCorrection = clampedPowerDelta };
+
+ var correction = powerDifference / 4; //TODO magic number to config
+
+
+ // find out if we reach the lower or upper Dc limit by comparing the current Dc voltage to the reference voltage
+ return s.AcDc.Dc.Voltage > s.Config.GridTie.AcDc.ReferenceDcLinkVoltage
+ ? control with { PowerCorrection = clampedPowerDelta.ClampMax(-correction), LimitedBy = EssLimit.ChargeLimitedByMaxDcBusVoltage }
+ : control with { PowerCorrection = clampedPowerDelta.ClampMin(correction), LimitedBy = EssLimit.DischargeLimitedByMinDcBusVoltage };
+ }
+
+
+ private static EssControl LimitChargePower(this EssControl control, StatusRecord s)
+ {
+
+ //var maxInverterChargePower = s.ControlInverterPower(s.Config.MaxInverterPower);
+ var maxBatteryChargePower = s.MaxBatteryChargePower();
+ maxBatteryChargePower.WriteLine(" Max Battery Charge Power");
+
+ return control
+ //.LimitChargePower(, EssLimit.ChargeLimitedByInverterPower)
+ .LimitChargePower(maxBatteryChargePower, EssLimit.ChargeLimitedByBatteryPower);
+
+ }
+
+ private static EssControl LimitDischargePower(this EssControl control, StatusRecord s)
+ {
+ var maxBatteryDischargeDelta = s.Battery?.Devices.Count * MaxDischargePower ?? 0;
+ var keepMinSocLimitDelta = s.ControlBatteryPower(s.HoldMinSocPower());
+ maxBatteryDischargeDelta.WriteLine(" Max Battery Discharge Power");
+
+
+ return control
+ .LimitDischargePower(maxBatteryDischargeDelta , EssLimit.DischargeLimitedByBatteryPower)
+ .LimitDischargePower(keepMinSocLimitDelta , EssLimit.DischargeLimitedByMinSoc);
+ }
+
+ private static Double ComputePowerDelta(this StatusRecord s, EssMode mode)
+ {
+ var chargePower = s.AcDc.Devices.Sum(d => d.Status.Nominal.Power.Value);
+
+
+ s.Config.GridSetPoint.WriteLine(" GridSetPoint");
+
+ return mode switch
+ {
+ EssMode.ReachMinSoc => s.ControlInverterPower(chargePower),
+ EssMode.OptimizeSelfConsumption => s.ControlGridPower(s.Config.GridSetPoint),
+ EssMode.Off => 0,
+ EssMode.OffGrid => 0,
+ EssMode.NoGridMeter => 0,
+ _ => throw new ArgumentException(null, nameof(mode))
+ };
+ }
+
+ // private static Boolean MustHeatBatteries(this StatusRecord s)
+ // {
+ // var batteries = s.GetBatteries();
+//
+ // if (batteries.Count <= 0)
+ // return true; // batteries might be there but BMS is without power
+//
+ // return batteries
+ // .Select(b => b.Temperatures.State)
+ // .Contains(TemperatureState.Cold);
+ // }
+
+ private static Double MaxBatteryChargePower(this StatusRecord s)
+ {
+ // This introduces a limit when we don't have communication with batteries
+ // Otherwise the limit will be 0 and the batteries will be not heated
+
+ var batteries = s.GetBatteries();
+
+ var maxChargePower = batteries.Count == 0
+ ? 0
+ : batteries.Count * MaxChargePower;
+
+ return maxChargePower;
+ }
+
+ private static Double CurrentPowerSetPoint(this StatusRecord s)
+ {
+ return s
+ .AcDc
+ .Devices
+ .Select(d =>
+ {
+ var acPowerControl = d.Control.Ac.Power;
+
+ return acPowerControl.L1.Active
+ + acPowerControl.L2.Active
+ + acPowerControl.L3.Active;
+ })
+ .Sum(p => p);
+ }
+
+ private static Boolean MustReachMinSoc(this StatusRecord s)
+ {
+ var batteries = s.GetBatteries();
+
+ return batteries.Count > 0
+ && batteries.Any(b => b.BatteryDeligreenDataRecord.Soc < s.Config.MinSoc);
+ }
+
+ private static IReadOnlyList GetBatteries(this StatusRecord s)
+ {
+ return s.Battery?.Devices ?? Array.Empty();
+ }
+
+ private static Double ControlGridPower(this StatusRecord status, Double targetPower)
+ {
+ return ControlPower
+ (
+ measurement : status.GridMeter!.Ac.Power.Active,
+ target : targetPower,
+ pConstant : status.Config.PConstant
+ );
+ }
+
+ private static Double ControlInverterPower(this StatusRecord status, Double targetInverterPower)
+ {
+ return ControlPower
+ (
+ measurement : status.AcDc.Ac.Power.Active,
+ target : targetInverterPower,
+ pConstant : status.Config.PConstant
+ );
+ }
+
+ private static Double ControlBatteryPower(this StatusRecord status, Double targetBatteryPower)
+ {
+ return ControlPower
+ (
+ measurement: status.GetBatteries().Sum(b => b.BatteryDeligreenDataRecord.Power),
+ target: targetBatteryPower,
+ pConstant: status.Config.PConstant
+ );
+ }
+
+ private static Double HoldMinSocPower(this StatusRecord s)
+ {
+ // TODO: explain LowSOC curve
+
+ var batteries = s.GetBatteries();
+
+ if (batteries.Count == 0)
+ return Double.NegativeInfinity;
+
+ var a = -2 * s.Config.BatterySelfDischargePower * batteries.Count / s.Config.HoldSocZone;
+ var b = -a * (s.Config.MinSoc + s.Config.HoldSocZone);
+
+ return batteries.Min(d => d.BatteryDeligreenDataRecord.Soc.Value) * a + b;
+ }
+
+ private static Double ControlPower(Double measurement, Double target, Double pConstant)
+ {
+ var error = target - measurement;
+ return error * pConstant;
+ }
+
+ // ReSharper disable once UnusedMember.Local, TODO
+ private static Double ControlPowerWithIntegral(Double measurement, Double target, Double p, Double i)
+ {
+ var errorSum = 0; // this is must be sum of error
+ var error = target - measurement;
+ var kp = p * error;
+ var ki = i * errorSum;
+ return ki + kp;
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Ess/EssControl.cs b/csharp/App/SodiStoreMax/src/Ess/EssControl.cs
new file mode 100644
index 000000000..66c3b3681
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Ess/EssControl.cs
@@ -0,0 +1,53 @@
+using InnovEnergy.Lib.Units.Power;
+
+namespace InnovEnergy.App.SodiStoreMax.Ess;
+
+public record EssControl
+{
+ public required EssMode Mode { get; init; }
+ public required EssLimit LimitedBy { get; init; }
+ public required ActivePower PowerCorrection { get; init; }
+ public required ActivePower PowerSetpoint { get; init; }
+
+ public static EssControl Default { get; } = new()
+ {
+ Mode = EssMode.Off,
+ LimitedBy = EssLimit.NoLimit,
+ PowerCorrection = 0,
+ PowerSetpoint = 0
+ };
+
+
+ public EssControl LimitChargePower(Double controlDelta, EssLimit reason)
+ {
+ var overload = PowerCorrection - controlDelta;
+
+ if (overload <= 0)
+ return this;
+
+ return this with
+ {
+ LimitedBy = reason,
+ PowerCorrection = controlDelta,
+ PowerSetpoint = PowerSetpoint - overload
+ };
+ }
+
+ public EssControl LimitDischargePower(Double controlDelta, EssLimit reason)
+ {
+ var overload = PowerCorrection - controlDelta;
+
+ if (overload >= 0)
+ return this;
+
+ return this with
+ {
+ LimitedBy = reason,
+ PowerCorrection = controlDelta,
+ PowerSetpoint = PowerSetpoint - overload
+ };
+ }
+}
+
+
+
diff --git a/csharp/App/SodiStoreMax/src/Ess/EssLimit.cs b/csharp/App/SodiStoreMax/src/Ess/EssLimit.cs
new file mode 100644
index 000000000..4a814a790
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Ess/EssLimit.cs
@@ -0,0 +1,20 @@
+namespace InnovEnergy.App.SodiStoreMax.Ess;
+
+public enum EssLimit
+{
+ NoLimit,
+ DischargeLimitedByMinSoc,
+ DischargeLimitedByBatteryPower,
+ DischargeLimitedByInverterPower,
+ ChargeLimitedByInverterPower,
+ ChargeLimitedByBatteryPower,
+ ChargeLimitedByMaxDcBusVoltage,
+ DischargeLimitedByMinDcBusVoltage,
+}
+
+
+// limitedBy = $"limiting discharging power in order to stay above min SOC: {s.Config.MinSoc}%";
+// limitedBy = $"limited by max battery discharging power: {maxDischargePower}";
+// limitedBy = $"limited by max inverter Dc to Ac power: {-s.Config.MaxInverterPower}W";
+// limitedBy = $"limited by max battery charging power: {maxChargePower}";
+// limitedBy = "limited by max inverter Ac to Dc power";
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Ess/EssMode.cs b/csharp/App/SodiStoreMax/src/Ess/EssMode.cs
new file mode 100644
index 000000000..c81c5953c
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Ess/EssMode.cs
@@ -0,0 +1,12 @@
+namespace InnovEnergy.App.SodiStoreMax.Ess;
+
+public enum EssMode
+{
+ Off,
+ OffGrid,
+ HeatBatteries,
+ CalibrationCharge,
+ ReachMinSoc,
+ NoGridMeter,
+ OptimizeSelfConsumption
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Ess/SalimaxAlarmState.cs b/csharp/App/SodiStoreMax/src/Ess/SalimaxAlarmState.cs
new file mode 100644
index 000000000..1cd7b3bd6
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Ess/SalimaxAlarmState.cs
@@ -0,0 +1,8 @@
+namespace InnovEnergy.App.SodiStoreMax.Ess;
+
+public enum SalimaxAlarmState
+{
+ Green,
+ Orange,
+ Red
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Ess/StatusRecord.cs b/csharp/App/SodiStoreMax/src/Ess/StatusRecord.cs
new file mode 100644
index 000000000..151ee0ff4
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Ess/StatusRecord.cs
@@ -0,0 +1,33 @@
+using InnovEnergy.App.SodiStoreMax.Devices;
+using InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+using InnovEnergy.App.SodiStoreMax.System;
+using InnovEnergy.App.SodiStoreMax.SystemConfig;
+using InnovEnergy.Lib.Devices.AMPT;
+using InnovEnergy.Lib.Devices.BatteryDeligreen;
+using InnovEnergy.Lib.Devices.EmuMeter;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
+
+namespace InnovEnergy.App.SodiStoreMax.Ess;
+
+public record StatusRecord
+{
+ public required AcDcDevicesRecord AcDc { get; init; }
+ public required DcDcDevicesRecord DcDc { get; init; }
+ public required BatteryDeligreenRecords? Battery { get; init; }
+ public required EmuMeterRegisters? GridMeter { get; init; }
+ public required EmuMeterRegisters? LoadOnAcIsland { get; init; }
+ public required AcPowerDevice? LoadOnAcGrid { get; init; }
+ public required AmptStatus? PvOnAcGrid { get; init; }
+ public required AmptStatus? PvOnAcIsland { get; init; }
+ public required AcPowerDevice? AcGridToAcIsland { get; init; }
+ public required DcPowerDevice? AcDcToDcLink { get; init; }
+ public required DcPowerDevice? LoadOnDc { get; init; }
+ public required IRelaysRecord? Relays { get; init; }
+ public required AmptStatus? PvOnDc { get; init; }
+ public required Config Config { get; set; }
+ public required SystemLog Log { get; init; } // TODO: init only
+
+ public required EssControl EssControl { get; set; } // TODO: init only
+ public required StateMachine StateMachine { get; init; }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Ess/SystemLog.cs b/csharp/App/SodiStoreMax/src/Ess/SystemLog.cs
new file mode 100644
index 000000000..71b58a9af
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Ess/SystemLog.cs
@@ -0,0 +1,11 @@
+using InnovEnergy.App.SodiStoreMax.DataTypes;
+
+namespace InnovEnergy.App.SodiStoreMax.Ess;
+
+public record SystemLog
+{
+ public required String? Message { get; init; }
+ public required SalimaxAlarmState SalimaxAlarmState { get; init; }
+ public required List? SalimaxAlarms { get; set; }
+ public required List? SalimaxWarnings { get; set; }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Flow.cs b/csharp/App/SodiStoreMax/src/Flow.cs
new file mode 100644
index 000000000..99e0ff452
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Flow.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics.CodeAnalysis;
+using InnovEnergy.Lib.Units;
+using InnovEnergy.Lib.Utils;
+
+
+namespace InnovEnergy.App.SodiStoreMax;
+
+public static class Flow
+{
+ private static readonly String RightArrowChar = ">";
+ private static readonly String LeftArrowChar = "<";
+ private static readonly String DownArrowChar = "V";
+ private static readonly String UpArrowChar = "^";
+ private static readonly String UnknownArrowChar = "?";
+
+ public static TextBlock Horizontal(Unit? amount) => Horizontal(amount, 10);
+
+ public static TextBlock Horizontal(Unit? amount, Int32 width)
+ {
+ var label = amount?.ToDisplayString() ?? "";
+
+ var arrowChar = amount switch
+ {
+ { Value: < 0 } => LeftArrowChar,
+ { Value: >= 0 } => RightArrowChar,
+ _ => UnknownArrowChar,
+ };
+
+ //var arrowChar = amount.Value < 0 ? LeftArrowChar : RightArrowChar;
+ var arrow = Enumerable.Repeat(arrowChar, width).Join();
+
+ // note : appending "fake label" below to make it vertically symmetric
+ return TextBlock.AlignCenterHorizontal(label, arrow, "");
+ }
+
+ public static TextBlock Vertical(Unit? amount) => Vertical(amount, 4);
+
+ [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
+ [SuppressMessage("ReSharper", "CoVariantArrayConversion")]
+ public static TextBlock Vertical(Unit? amount, Int32 height)
+ {
+ var label = amount?.ToDisplayString() ?? UnknownArrowChar;
+ var arrowChar = amount switch
+ {
+ { Value: < 0 } => UpArrowChar,
+ { Value: >= 0 } => DownArrowChar,
+ _ => UnknownArrowChar,
+ };
+
+ // var arrowChar = amount is null ? UnknownArrowChar
+ // : amount.Value < 0 ? UpArrowChar
+ // : DownArrowChar;
+
+ return TextBlock.AlignCenterHorizontal(arrowChar, arrowChar, label, arrowChar, arrowChar);
+ }
+}
diff --git a/csharp/App/SodiStoreMax/src/LogFileConcatenator.cs b/csharp/App/SodiStoreMax/src/LogFileConcatenator.cs
new file mode 100644
index 000000000..9ee7b4e41
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/LogFileConcatenator.cs
@@ -0,0 +1,34 @@
+using System.Text;
+
+namespace InnovEnergy.App.SodiStoreMax;
+
+public class LogFileConcatenator
+{
+ private readonly string _logDirectory;
+
+ public LogFileConcatenator(String logDirectory = "LogDirectory/")
+ {
+ _logDirectory = logDirectory;
+ }
+
+ public String ConcatenateFiles(int numberOfFiles)
+ {
+ var logFiles = Directory
+ .GetFiles(_logDirectory, "log_*.csv")
+ .OrderByDescending(file => file)
+ .Take(numberOfFiles)
+ .OrderBy(file => file)
+ .ToList();
+
+ var concatenatedContent = new StringBuilder();
+
+ foreach (var fileContent in logFiles.Select(File.ReadAllText))
+ {
+ concatenatedContent.AppendLine(fileContent);
+ //concatenatedContent.AppendLine(); // Append an empty line to separate the files // maybe we don't need this
+ }
+
+ return concatenatedContent.ToString();
+ }
+}
+
diff --git a/csharp/App/SodiStoreMax/src/Logfile.cs b/csharp/App/SodiStoreMax/src/Logfile.cs
new file mode 100644
index 000000000..75739b56d
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Logfile.cs
@@ -0,0 +1,49 @@
+using InnovEnergy.Lib.Utils;
+using Microsoft.Extensions.Logging;
+
+namespace InnovEnergy.App.SodiStoreMax;
+
+public class CustomLogger : ILogger
+{
+ private readonly String _logFilePath;
+ //private readonly Int64 _maxFileSizeBytes;
+ private readonly Int32 _maxLogFileCount;
+ private Int64 _currentFileSizeBytes;
+
+ public CustomLogger(String logFilePath, Int32 maxLogFileCount)
+ {
+ _logFilePath = logFilePath;
+ _maxLogFileCount = maxLogFileCount;
+ _currentFileSizeBytes = File.Exists(logFilePath) ? new FileInfo(logFilePath).Length : 0;
+ }
+
+ public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException();
+
+ public Boolean IsEnabled(LogLevel logLevel) => true; // Enable logging for all levels
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ var logMessage = formatter(state, exception!);
+
+ // Check the log file count and delete the oldest file if necessary
+ var logFileDir = Path.GetDirectoryName(_logFilePath)!;
+ var logFileExt = Path.GetExtension(_logFilePath);
+ var logFileBaseName = Path.GetFileNameWithoutExtension(_logFilePath);
+
+ var logFiles = Directory
+ .GetFiles(logFileDir, $"{logFileBaseName}_*{logFileExt}")
+ .OrderBy(file => file)
+ .ToList();
+
+ if (logFiles.Count >= _maxLogFileCount)
+ {
+ File.Delete(logFiles.First());
+ }
+
+ var roundedUnixTimestamp = DateTime.Now.ToUnixTime() % 2 == 0 ? DateTime.Now.ToUnixTime() : DateTime.Now.ToUnixTime() + 1;
+ var timestamp = "Timestamp;" + roundedUnixTimestamp + Environment.NewLine;
+
+ var logFileBackupPath = Path.Combine(logFileDir, $"{logFileBaseName}_{DateTime.Now.ToUnixTime()}{logFileExt}");
+ File.AppendAllText(logFileBackupPath, timestamp + logMessage + Environment.NewLine);
+ }
+}
diff --git a/csharp/App/SodiStoreMax/src/Logger.cs b/csharp/App/SodiStoreMax/src/Logger.cs
new file mode 100644
index 000000000..fadd6babe
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Logger.cs
@@ -0,0 +1,40 @@
+using InnovEnergy.App.SodiStoreMax;
+using Microsoft.Extensions.Logging;
+
+namespace InnovEnergy.App.SodiStoreMax;
+
+public static class Logger
+{
+ // Specify the maximum log file size in bytes (e.g., 1 MB)
+
+ //private const Int32 MaxFileSizeBytes = 2024 * 30; // TODO: move to settings
+ private const Int32 MaxLogFileCount = 5000; // TODO: move to settings
+ private const String LogFilePath = "LogDirectory/log.csv"; // TODO: move to settings
+
+ // ReSharper disable once InconsistentNaming
+ private static readonly ILogger _logger = new CustomLogger(LogFilePath, MaxLogFileCount);
+
+ public static T LogInfo(this T t) where T : notnull
+ {
+ _logger.LogInformation(t.ToString()); // TODO: check warning
+ return t;
+ }
+
+ public static T LogDebug(this T t) where T : notnull
+ {
+ _logger.LogDebug(t.ToString()); // TODO: check warning
+ return t;
+ }
+
+ public static T LogError(this T t) where T : notnull
+ {
+ _logger.LogError(t.ToString()); // TODO: check warning
+ return t;
+ }
+
+ public static T LogWarning(this T t) where T : notnull
+ {
+ _logger.LogWarning(t.ToString()); // TODO: check warning
+ return t;
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/MiddlewareClasses/MiddlewareAgent.cs b/csharp/App/SodiStoreMax/src/MiddlewareClasses/MiddlewareAgent.cs
new file mode 100644
index 000000000..6e1bd8c36
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/MiddlewareClasses/MiddlewareAgent.cs
@@ -0,0 +1,93 @@
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.Json;
+using InnovEnergy.App.SodiStoreMax.DataTypes;
+
+namespace InnovEnergy.App.SodiStoreMax.MiddlewareClasses;
+
+public static class MiddlewareAgent
+{
+ private static UdpClient _udpListener = null!;
+ private static IPAddress? _controllerIpAddress;
+ private static EndPoint? _endPoint;
+
+ public static void InitializeCommunicationToMiddleware()
+ {
+ _controllerIpAddress = FindVpnIp();
+ if (Equals(IPAddress.None, _controllerIpAddress))
+ {
+ Console.WriteLine("There is no VPN interface, exiting...");
+ }
+
+ const Int32 udpPort = 9000;
+ _endPoint = new IPEndPoint(_controllerIpAddress, udpPort);
+
+ _udpListener = new UdpClient();
+ _udpListener.Client.Blocking = false;
+ _udpListener.Client.Bind(_endPoint);
+ }
+
+ private static IPAddress FindVpnIp()
+ {
+ const String interfaceName = "innovenergy";
+
+ var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
+
+ foreach (var networkInterface in networkInterfaces)
+ {
+ if (networkInterface.Name == interfaceName)
+ {
+ var ipProps = networkInterface.GetIPProperties();
+ var uniCastIPs = ipProps.UnicastAddresses;
+ var controllerIpAddress = uniCastIPs[0].Address;
+
+ Console.WriteLine("VPN IP is: "+ uniCastIPs[0].Address);
+ return controllerIpAddress;
+ }
+ }
+
+ return IPAddress.None;
+ }
+
+ public static Configuration? SetConfigurationFile()
+ {
+ if (_udpListener.Available > 0)
+ {
+
+ IPEndPoint? serverEndpoint = null;
+
+ var replyMessage = "ACK";
+ var replyData = Encoding.UTF8.GetBytes(replyMessage);
+
+ var udpMessage = _udpListener.Receive(ref serverEndpoint);
+ var message = Encoding.UTF8.GetString(udpMessage);
+
+ var config = JsonSerializer.Deserialize(message);
+
+ if (config != null)
+ {
+ Console.WriteLine($"Received a configuration message: GridSetPoint is " + config.GridSetPoint +
+ ", MinimumSoC is " + config.MinimumSoC + " and ForceCalibrationCharge is " +
+ config.CalibrationChargeState + " and CalibrationChargeDate is " +
+ config.CalibrationChargeDate);
+
+ // Send the reply to the sender's endpoint
+ _udpListener.Send(replyData, replyData.Length, serverEndpoint);
+ Console.WriteLine($"Replied to {serverEndpoint}: {replyMessage}");
+ return config;
+ }
+ }
+
+ if (_endPoint != null && !_endPoint.Equals(_udpListener.Client.LocalEndPoint as IPEndPoint))
+ {
+ Console.WriteLine("UDP address has changed, rebinding...");
+ InitializeCommunicationToMiddleware();
+ }
+
+
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/MiddlewareClasses/RabbitMQManager.cs b/csharp/App/SodiStoreMax/src/MiddlewareClasses/RabbitMQManager.cs
new file mode 100644
index 000000000..2de9f5665
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/MiddlewareClasses/RabbitMQManager.cs
@@ -0,0 +1,61 @@
+using System.Text;
+using System.Text.Json;
+using InnovEnergy.App.SodiStoreMax.DataTypes;
+using RabbitMQ.Client;
+
+namespace InnovEnergy.App.SodiStoreMax.MiddlewareClasses;
+
+public static class RabbitMqManager
+{
+ public static ConnectionFactory? Factory ;
+ public static IConnection ? Connection;
+ public static IModel? Channel;
+
+ public static Boolean SubscribeToQueue(StatusMessage currentSalimaxState, String? s3Bucket,String VpnServerIp)
+ {
+ try
+ {
+ //_factory = new ConnectionFactory { HostName = VpnServerIp };
+
+ Factory = new ConnectionFactory
+ {
+ HostName = VpnServerIp,
+ Port = 5672,
+ VirtualHost = "/",
+ UserName = "producer",
+ Password = "b187ceaddb54d5485063ddc1d41af66f",
+
+ };
+
+ Connection = Factory.CreateConnection();
+ Channel = Connection.CreateModel();
+ Channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null);
+
+ Console.WriteLine("The controller sends its status to the middleware for the first time");
+ if (s3Bucket != null) InformMiddleware(currentSalimaxState);
+
+
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("An error occurred while connecting to the RabbitMQ queue: " + ex.Message);
+ return false;
+ }
+ return true;
+ }
+
+ public static void InformMiddleware(StatusMessage status)
+ {
+ var message = JsonSerializer.Serialize(status);
+ var body = Encoding.UTF8.GetBytes(message);
+
+ Channel.BasicPublish(exchange: string.Empty,
+ routingKey: "statusQueue",
+ basicProperties: null,
+ body: body);
+
+ Console.WriteLine($"Producer sent message: {message}");
+ }
+
+
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Program.cs b/csharp/App/SodiStoreMax/src/Program.cs
new file mode 100644
index 000000000..10e3ca99e
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Program.cs
@@ -0,0 +1,962 @@
+#undef Amax
+#undef GridLimit
+
+using System.Diagnostics;
+using System.IO.Compression;
+using System.Reactive.Linq;
+using System.Reactive.Threading.Tasks;
+using System.Reflection.Metadata;
+using System.Security;
+using System.Text;
+using Flurl.Http;
+using InnovEnergy.App.SodiStoreMax;
+using InnovEnergy.App.SodiStoreMax.Devices;
+using InnovEnergy.App.SodiStoreMax.Ess;
+using InnovEnergy.App.SodiStoreMax.MiddlewareClasses;
+using InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+using InnovEnergy.App.SodiStoreMax.System;
+using InnovEnergy.App.SodiStoreMax.SystemConfig;
+using InnovEnergy.Lib.Devices.AMPT;
+using InnovEnergy.Lib.Devices.BatteryDeligreen;
+using InnovEnergy.Lib.Devices.EmuMeter;
+using InnovEnergy.Lib.Devices.Trumpf.SystemControl;
+using InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc.DataTypes;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc.Control;
+using InnovEnergy.Lib.Protocols.Modbus.Channels;
+using InnovEnergy.Lib.Units;
+using InnovEnergy.Lib.Utils;
+using InnovEnergy.App.SodiStoreMax.DataTypes;
+using InnovEnergy.Lib.Utils.Net;
+using static System.Int32;
+using static InnovEnergy.App.SodiStoreMax.AggregationService.Aggregator;
+using static InnovEnergy.App.SodiStoreMax.MiddlewareClasses.MiddlewareAgent;
+using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.SystemConfig;
+using DeviceState = InnovEnergy.App.SodiStoreMax.Devices.DeviceState;
+
+#pragma warning disable IL2026
+
+namespace InnovEnergy.App.SodiStoreMax;
+
+internal static class Program
+{
+ private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(2);
+
+ private static readonly IReadOnlyList BatteryNodes;
+
+ private static readonly Channel TruConvertAcChannel;
+ private static readonly Channel TruConvertDcChannel;
+ private static readonly Channel GridMeterChannel;
+ private static readonly Channel IslandBusLoadChannel;
+ private static readonly Channel PvOnDc;
+ private static readonly Channel PvOnAcGrid;
+ private static readonly Channel PvOnAcIsland;
+ private static readonly Channel RelaysChannel;
+ private static readonly Channel RelaysTsChannel;
+ private static readonly Channel BatteriesChannel;
+
+ private static Boolean _curtailFlag = false;
+ private const String VpnServerIp = "10.2.0.11";
+ private static Boolean _subscribedToQueue = false;
+ private static Boolean _subscribeToQueueForTheFirstTime = false;
+ private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green;
+ private const UInt16 NbrOfFileToConcatenate = 30;
+ private static UInt16 _counterOfFile = 0;
+ private static SalimaxAlarmState _salimaxAlarmState = SalimaxAlarmState.Green;
+ private const String Port = "/dev/ttyUSB0";
+
+
+ static Program()
+ {
+ var config = Config.Load();
+ var d = config.Devices;
+
+ Channel CreateChannel(SalimaxDevice device) => device.DeviceState == DeviceState.Disabled
+ ? new NullChannel()
+ : new TcpChannel(device);
+
+
+ TruConvertAcChannel = CreateChannel(d.TruConvertAcIp);
+ TruConvertDcChannel = CreateChannel(d.TruConvertDcIp);
+ GridMeterChannel = CreateChannel(d.GridMeterIp);
+ IslandBusLoadChannel = CreateChannel(d.IslandBusLoadMeterIp);
+ PvOnDc = CreateChannel(d.PvOnDc);
+ PvOnAcGrid = CreateChannel(d.PvOnAcGrid);
+ PvOnAcIsland = CreateChannel(d.PvOnAcIsland);
+ RelaysChannel = CreateChannel(d.RelaysIp);
+ RelaysTsChannel = CreateChannel(d.TsRelaysIp);
+ BatteriesChannel = CreateChannel(d.BatteryIp);
+
+ BatteryNodes = config
+ .Devices
+ .BatteryNodes
+ .Select(n => n.ConvertTo())
+ .ToArray(config.Devices.BatteryNodes.Length);
+ }
+
+ public static async Task Main(String[] args)
+ {
+ //Do not await
+ HourlyDataAggregationManager()
+ .ContinueWith(t=>t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted)
+ .SupressAwaitWarning();
+
+ DailyDataAggregationManager()
+ .ContinueWith(t=>t.Exception.WriteLine(), TaskContinuationOptions.OnlyOnFaulted)
+ .SupressAwaitWarning();
+
+ InitializeCommunicationToMiddleware();
+
+ while (true)
+ {
+ try
+ {
+ await Run();
+ }
+ catch (Exception e)
+ {
+ e.LogError();
+ }
+ }
+ }
+
+
+ private static async Task Run()
+ {
+ "Starting SodiStore Max".WriteLine();
+
+ Watchdog.NotifyReady();
+
+ var batteryDeligreenDevice = BatteryNodes.Select(n => new BatteryDeligreenDevice(Port, n))
+ .ToList();
+
+ var batteryDevices = new BatteryDeligreenDevices(batteryDeligreenDevice);
+ var acDcDevices = new TruConvertAcDcDevices(TruConvertAcChannel);
+ var dcDcDevices = new TruConvertDcDcDevices(TruConvertDcChannel);
+ var gridMeterDevice = new EmuMeterDevice(GridMeterChannel);
+ var acIslandLoadMeter = new EmuMeterDevice(IslandBusLoadChannel);
+ var pvOnDcDevice = new AmptDevices(PvOnDc);
+ var pvOnAcGridDevice = new AmptDevices(PvOnAcGrid);
+ var pvOnAcIslandDevice = new AmptDevices(PvOnAcIsland);
+ var saliMaxTsRelaysDevice = new RelaysDeviceAdam6060(RelaysTsChannel);
+
+
+#if Amax
+ var saliMaxRelaysDevice = new RelaysDeviceAmax(RelaysChannel);
+#else
+ var saliMaxRelaysDevice = new RelaysDeviceAdam6360(RelaysChannel);
+#endif
+
+
+ StatusRecord ReadStatus()
+ {
+ var config = Config.Load();
+ var devices = config.Devices;
+ var acDc = acDcDevices.Read();
+ var dcDc = dcDcDevices.Read();
+ var relays = saliMaxRelaysDevice.Read();
+ var tsRelays = saliMaxTsRelaysDevice.Read();
+ var loadOnAcIsland = acIslandLoadMeter.Read();
+ var gridMeter = gridMeterDevice.Read();
+ var pvOnDc = pvOnDcDevice.Read();
+ var battery = batteryDevices.Read();
+
+ var pvOnAcGrid = pvOnAcGridDevice.Read();
+ var pvOnAcIsland = pvOnAcIslandDevice.Read();
+
+ var gridBusToIslandBus = Topology.CalculateGridBusToIslandBusPower(pvOnAcIsland, loadOnAcIsland, acDc);
+
+ var gridBusLoad = devices.LoadOnAcGrid.DeviceState == DeviceState.Disabled
+ ? new AcPowerDevice { Power = 0 }
+ : Topology.CalculateGridBusLoad(gridMeter, pvOnAcGrid, gridBusToIslandBus);
+
+ var dcLoad = devices.LoadOnDc.DeviceState == DeviceState.Disabled
+ ? new DcPowerDevice { Power = 0 }
+ : Topology.CalculateDcLoad(acDc, pvOnDc, dcDc);
+
+ var acDcToDcLink = devices.LoadOnDc.DeviceState == DeviceState.Disabled ?
+ Topology.CalculateAcDcToDcLink(pvOnDc, dcDc, acDc)
+ : new DcPowerDevice{ Power = acDc.Dc.Power};
+
+#if Amax
+ var combinedRelays = relays;
+#else
+ var combinedRelays = new CombinedAdamRelaysRecord(tsRelays, relays);
+#endif
+
+ return new StatusRecord
+ {
+ AcDc = acDc,
+ DcDc = dcDc,
+ Battery = battery,
+ Relays = combinedRelays,
+ GridMeter = gridMeter,
+ PvOnAcGrid = pvOnAcGrid,
+ PvOnAcIsland = pvOnAcIsland,
+ PvOnDc = pvOnDc,
+ AcGridToAcIsland = gridBusToIslandBus,
+ AcDcToDcLink = acDcToDcLink,
+ LoadOnAcGrid = gridBusLoad,
+ LoadOnAcIsland = loadOnAcIsland,
+ LoadOnDc = dcLoad,
+ StateMachine = StateMachine.Default,
+ EssControl = EssControl.Default,
+ Log = new SystemLog { SalimaxAlarmState = SalimaxAlarmState.Green, Message = null, SalimaxAlarms = null, SalimaxWarnings = null}, //TODO: Put real stuff
+ Config = config // load from disk every iteration, so config can be changed while running
+ };
+ }
+
+ void WriteControl(StatusRecord r)
+ {
+ if (r.Relays is not null)
+ {
+#if Amax
+ saliMaxRelaysDevice.Write((RelaysRecordAmax)r.Relays);
+#else
+
+ if (r.Relays is CombinedAdamRelaysRecord adamRelays)
+ {
+ saliMaxRelaysDevice.Write(adamRelays.GetAdam6360DRecord() ?? throw new InvalidOperationException());
+ saliMaxTsRelaysDevice.Write(adamRelays.GetAdam6060Record() ?? throw new InvalidOperationException());
+ }
+#endif
+ }
+
+ acDcDevices.Write(r.AcDc);
+ dcDcDevices.Write(r.DcDc);
+ }
+
+ Console.WriteLine("press ctrl-c to stop");
+
+ while (true)
+ {
+ await Observable
+ .Interval(UpdateInterval)
+ .Select(_ => RunIteration())
+ .SelectMany(r => UploadCsv(r, DateTime.Now.Round(UpdateInterval)))
+ .SelectError()
+ .ToTask();
+ }
+
+
+ StatusRecord RunIteration()
+ {
+ Watchdog.NotifyAlive();
+
+ var record = ReadStatus();
+ /*
+ if (record.Relays != null)
+ {
+ record.Relays.Do0StartPulse = true;
+
+ record.Relays.PulseOut0HighTime = 20000;
+ record.Relays.PulseOut0LowTime = 20000;
+ record.Relays.DigitalOutput0Mode = 2;
+
+ record.Relays.LedGreen = false;
+
+ record.Relays.Do0StartPulse.WriteLine(" = start pulse 0");
+
+ record.Relays.PulseOut0HighTime.WriteLine(" = PulseOut0HighTime");
+
+ record.Relays.PulseOut0LowTime.WriteLine(" = PulseOut0LowTime");
+
+ record.Relays.DigitalOutput0Mode.WriteLine(" = DigitalOutput0Mode");
+
+ record.Relays.LedGreen.WriteLine(" = LedGreen");
+
+ record.Relays.LedRed.WriteLine(" = LedRed");
+
+ }
+ else
+ {
+ " Relays are null".WriteLine();
+ }*/
+
+ SendSalimaxStateAlarm(GetSalimaxStateAlarm(record), record); // to improve
+
+ record.ControlConstants();
+ record.ControlSystemState();
+
+ record.ControlPvPower(record.Config.CurtailP, record.Config.PvInstalledPower);
+
+ var essControl = record.ControlEss().WriteLine();
+
+ record.EssControl = essControl;
+
+ record.AcDc.SystemControl.ApplyAcDcDefaultSettings();
+ record.DcDc.SystemControl.ApplyDcDcDefaultSettings();
+
+ DistributePower(record, essControl);
+
+ record.PerformLed();
+
+ WriteControl(record);
+
+ $"{DateTime.Now.Round(UpdateInterval).ToUnixTime()} : {record.StateMachine.State}: {record.StateMachine.Message}".WriteLine();
+
+ record.CreateTopologyTextBlock().WriteLine();
+
+ (record.Relays is null ? "No relay Data available" : record.Relays.FiWarning ? "Alert: Fi Warning Detected" : "No Fi Warning Detected").WriteLine();
+ (record.Relays is null ? "No relay Data available" : record.Relays.FiError ? "Alert: Fi Error Detected" : "No Fi Error Detected") .WriteLine();
+
+ record.Config.Save();
+
+ "===========================================".WriteLine();
+
+ return record;
+ }
+
+ // ReSharper disable once FunctionNeverReturns
+ }
+
+ private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState, StatusRecord record)
+ {
+ var s3Bucket = Config.Load().S3?.Bucket;
+ var subscribedNow = false;
+
+ //Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue
+ //_heartBitInterval++;
+
+ //When the controller boots, it tries to subscribe to the queue
+ if (_subscribeToQueueForTheFirstTime == false)
+ {
+ subscribedNow = true;
+ _subscribeToQueueForTheFirstTime = true;
+ _prevSalimaxState = currentSalimaxState.Status;
+ _subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp);
+ }
+
+ //If already subscribed to the queue and the status has been changed, update the queue
+ if (!subscribedNow && _subscribedToQueue && currentSalimaxState.Status != _prevSalimaxState)
+ {
+ _prevSalimaxState = currentSalimaxState.Status;
+ if (s3Bucket != null)
+ RabbitMqManager.InformMiddleware(currentSalimaxState);
+ }
+ // else if (_subscribedToQueue && _heartBitInterval >= 30)
+ // {
+ // //Send a heartbit to the backend
+ // Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
+ // _heartBitInterval = 0;
+ // currentSalimaxState.Type = MessageType.Heartbit;
+ //
+ // if (s3Bucket != null)
+ // RabbitMqManager.InformMiddleware(currentSalimaxState);
+ // }
+
+ //If there is an available message from the RabbitMQ Broker, apply the configuration file
+ Configuration? config = SetConfigurationFile();
+ if (config != null)
+ {
+ record.ApplyConfigFile(config);
+ }
+ }
+
+ // This preparing a message to send to salimax monitor
+ private static StatusMessage GetSalimaxStateAlarm(StatusRecord record)
+ {
+ var alarmCondition = record.DetectAlarmStates(); // this need to be emailed to support or customer
+ var s3Bucket = Config.Load().S3?.Bucket;
+
+ var alarmList = new List();
+ var warningList = new List();
+ var bAlarmList = new List();
+ var bWarningList = new List();
+
+ /*
+ if (record.Battery != null)
+ {
+ var i = 0;
+
+ foreach (var battery in record.Battery.Devices)
+ {
+ var devicesBatteryNode = record.Config.Devices.BatteryNodes[i];
+
+ if (battery.LimpBitMap == 0)
+ {
+ // "All String are Active".WriteLine();
+ }
+ else if (IsPowerOfTwo(battery.LimpBitMap))
+ {
+ "1 String is disabled".WriteLine();
+ Console.WriteLine(" ****************** ");
+
+ warningList.Add(new AlarmOrWarning
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-dd"),
+ Time = DateTime.Now.ToString("HH:mm:ss"),
+ CreatedBy = "Battery node" + devicesBatteryNode,
+ Description = "1 String is disabled"
+ });
+
+ bWarningList.Add("/"+i+1 + "/1 String is disabled"); // battery id instead ( i +1 ) of node id: requested from the frontend
+ }
+ else
+ {
+ "2 or more string are disabled".WriteLine();
+ Console.WriteLine(" ****************** ");
+
+ alarmList.Add(new AlarmOrWarning
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-dd"),
+ Time = DateTime.Now.ToString("HH:mm:ss"),
+ CreatedBy = "Battery node" + devicesBatteryNode,
+ Description = "2 or more string are disabled"
+ });
+ bAlarmList.Add(i +";2 or more string are disabled");
+ }
+
+ foreach (var warning in record.Battery.Warnings)
+ {
+ warningList.Add(new AlarmOrWarning
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-dd"),
+ Time = DateTime.Now.ToString("HH:mm:ss"),
+ CreatedBy = "Battery node" + devicesBatteryNode,
+ Description = warning
+ });
+ bWarningList.Add(i +";" + warning);
+ }
+
+ foreach (var alarm in battery.Alarms)
+ {
+ alarmList.Add(new AlarmOrWarning
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-dd"),
+ Time = DateTime.Now.ToString("HH:mm:ss"),
+ CreatedBy = "Battery node" + devicesBatteryNode,
+ Description = alarm
+ });
+ bWarningList.Add(i +";" + alarm);
+ }
+ i++;
+ }
+ }*/
+
+ if (alarmCondition is not null)
+ {
+ alarmCondition.WriteLine();
+
+ alarmList.Add(new AlarmOrWarning
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-dd"),
+ Time = DateTime.Now.ToString("HH:mm:ss"),
+ CreatedBy = "Salimax",
+ Description = alarmCondition
+ });
+ }
+
+ foreach (var alarm in record.AcDc.Alarms)
+ {
+ alarmList.Add(new AlarmOrWarning
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-dd"),
+ Time = DateTime.Now.ToString("HH:mm:ss"),
+ CreatedBy = "AcDc",
+ Description = alarm.ToString()
+ });
+ }
+
+ foreach (var alarm in record.DcDc.Alarms)
+ {
+ alarmList.Add(new AlarmOrWarning
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-dd"),
+ Time = DateTime.Now.ToString("HH:mm:ss"),
+ CreatedBy = "DcDc",
+ Description = alarm.ToString()
+ });
+ }
+
+ foreach (var warning in record.AcDc.Warnings)
+ {
+ warningList.Add(new AlarmOrWarning
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-dd"),
+ Time = DateTime.Now.ToString("HH:mm:ss"),
+ CreatedBy = "AcDc",
+ Description = warning.ToString()
+ });
+ }
+
+ foreach (var warning in record.DcDc.Warnings)
+ {
+ warningList.Add(new AlarmOrWarning
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-dd"),
+ Time = DateTime.Now.ToString("HH:mm:ss"),
+ CreatedBy = "DcDc",
+ Description = warning.ToString()
+ });
+ }
+
+ _salimaxAlarmState = warningList.Any()
+ ? SalimaxAlarmState.Orange
+ : SalimaxAlarmState.Green; // this will be replaced by LedState
+
+ _salimaxAlarmState = alarmList.Any()
+ ? SalimaxAlarmState.Red
+ : _salimaxAlarmState; // this will be replaced by LedState
+
+ TryParse(s3Bucket?.Split("-")[0], out var installationId);
+
+ var returnedStatus = new StatusMessage
+ {
+ InstallationId = installationId,
+ Product = 0,
+ Status = _salimaxAlarmState,
+ Type = MessageType.AlarmOrWarning,
+ Alarms = alarmList,
+ Warnings = warningList
+ };
+
+ return returnedStatus;
+ }
+
+ private static String? DetectAlarmStates(this StatusRecord r) => r.Relays switch
+ {
+ { K2ConnectIslandBusToGridBus: false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: R0 is opening the K2 but the K2 is still close ",
+ { K1GridBusIsConnectedToGrid : false, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: K1 is open but the K2 is still close ",
+ { FiError: true, K2IslandBusIsConnectedToGridBus: true } => " Contradiction: Fi error occured but the K2 is still close ",
+ _ => null
+ };
+
+ private static void ControlConstants(this StatusRecord r)
+ {
+ var inverters = r.AcDc.Devices;
+ var dcDevices = r.DcDc.Devices;
+ var configFile = r.Config;
+ //var maxBatteryDischargingCurrentLive = 0.0; //never used with deligreenBattery
+ var devicesConfig = r.AcDc.Devices.All(d => d.Control.Ac.GridType == GridType.GridTied400V50Hz) ? configFile.GridTie : configFile.IslandMode; // TODO if any of the grid tie mode
+ /*
+ // This adapting the max discharging current to the current Active Strings
+ if (r.Battery != null)
+ {
+ const Int32 stringsByBattery = 5;
+ var numberOfBatteriesConfigured = r.Config.Devices.BatteryNodes.Length;
+ var numberOfTotalStrings = stringsByBattery * numberOfBatteriesConfigured;
+ var dischargingCurrentByString = devicesConfig.DcDc.MaxBatteryDischargingCurrent / numberOfTotalStrings;
+
+ var boolList = new List();
+
+ foreach (var stringActive in r.Battery.Devices.Select(b => b.BatteryStrings).ToList())
+ {
+ boolList.Add(stringActive.String1Active);
+ boolList.Add(stringActive.String2Active);
+ boolList.Add(stringActive.String3Active);
+ boolList.Add(stringActive.String4Active);
+ boolList.Add(stringActive.String5Active);
+ }
+
+ var numberOfBatteriesStringActive = boolList.Count(b => b);
+
+ if (numberOfTotalStrings != 0)
+ {
+ maxBatteryDischargingCurrentLive = dischargingCurrentByString * numberOfBatteriesStringActive;
+ }
+ }
+ */
+ // TODO The discharging current is well calculated but not communicated to live. But Written in S3
+
+
+ inverters.ForEach(d => d.Control.Dc.MaxVoltage = devicesConfig.AcDc.MaxDcLinkVoltage);
+ inverters.ForEach(d => d.Control.Dc.MinVoltage = devicesConfig.AcDc.MinDcLinkVoltage);
+ inverters.ForEach(d => d.Control.Dc.ReferenceVoltage = devicesConfig.AcDc.ReferenceDcLinkVoltage);
+
+ inverters.ForEach(d => d.Control.Dc.PrechargeConfig = DcPrechargeConfig.PrechargeDcWithInternal);
+
+ dcDevices.ForEach(d => d.Control.DroopControl.UpperVoltage = devicesConfig.DcDc.UpperDcLinkVoltage);
+ dcDevices.ForEach(d => d.Control.DroopControl.LowerVoltage = devicesConfig.DcDc.LowerDcLinkVoltage);
+ dcDevices.ForEach(d => d.Control.DroopControl.ReferenceVoltage = devicesConfig.DcDc.ReferenceDcLinkVoltage);
+
+ dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryChargingCurrent = devicesConfig.DcDc.MaxBatteryChargingCurrent);
+ dcDevices.ForEach(d => d.Control.CurrentControl.MaxBatteryDischargingCurrent = devicesConfig.DcDc.MaxBatteryDischargingCurrent);
+ dcDevices.ForEach(d => d.Control.MaxDcPower = devicesConfig.DcDc.MaxDcPower);
+
+ dcDevices.ForEach(d => d.Control.VoltageLimits.MaxBatteryVoltage = devicesConfig.DcDc.MaxChargeBatteryVoltage);
+ dcDevices.ForEach(d => d.Control.VoltageLimits.MinBatteryVoltage = devicesConfig.DcDc.MinDischargeBatteryVoltage);
+ dcDevices.ForEach(d => d.Control.ControlMode = DcControlMode.VoltageDroop);
+
+ r.DcDc.ResetAlarms();
+ r.AcDc.ResetAlarms();
+ }
+
+ // This will be used for provider throttling, this example is only for either 100% or 0 %
+ private static void ControlPvPower(this StatusRecord r, UInt16 exportLimit = 0, UInt16 pvInstalledPower = 20)
+ {
+ // Maybe add a condition to do this only if we are in optimised Self consumption, this is not true
+
+ if (r.GridMeter?.Ac.Power.Active == null)
+ {
+ Console.WriteLine(" No reading from Grid meter");
+ return;
+ }
+
+ if (pvInstalledPower == 0)
+ {
+ Console.WriteLine(" No curtailing, because Pv installed is equal to 0");
+ return;
+ }
+
+ const Int32 constantDeadBand = 5000; // magic number
+ const Double voltageRange = 100; // 100 Voltage configured rang for PV slope, if the configured slope change this must change also
+ var configFile = r.Config;
+ var inverters = r.AcDc.Devices;
+ var systemExportLimit = - exportLimit * 1000 ; // Conversion from Kw in W // the config file value is positive and limit should be negative from 0 to ...
+ var stepSize = ClampStepSize((UInt16)Math.Floor(voltageRange/ pvInstalledPower)); // in Voltage per 1 Kw
+ var deadBand = constantDeadBand/stepSize;
+
+ // LINQ query to select distinct ActiveUpperVoltage
+ var result = r.AcDc.Devices
+ .Select(device => device?.Status?.DcVoltages?.Active?.ActiveUpperVoltage)
+ .Select(voltage => voltage.Value) // Extract the value since we've confirmed it's non-null
+ .Distinct()
+ .ToList();
+
+ Double upperVoltage;
+
+ if (result.Count == 1)
+ {
+ upperVoltage = result[0];
+ }
+ else
+ {
+ Console.WriteLine(" Different ActiveUpperVoltage between inverters "); // this should be reported to salimax Alarm
+ return;
+ }
+
+ /************* For debugging purposes ********************/
+
+ systemExportLimit.WriteLine(" Export Limit in W");
+ upperVoltage.WriteLine(" Upper Voltage");
+ r.GridMeter.Ac.Power.Active.WriteLine(" Active Export");
+ Console.WriteLine(" ****************** ");
+
+ /*********************************************************/
+
+ if (r.GridMeter.Ac.Power.Active < systemExportLimit)
+ {
+ _curtailFlag = true;
+ upperVoltage = IncreaseInverterUpperLimit(upperVoltage, stepSize);
+ upperVoltage.WriteLine("Upper Voltage Increased: New Upper limit");
+ }
+ else
+ {
+ if (_curtailFlag)
+ {
+ if (r.GridMeter.Ac.Power.Active > (systemExportLimit + deadBand))
+ {
+ upperVoltage = DecreaseInverterUpperLimit(upperVoltage, stepSize);
+
+ if (upperVoltage <= configFile.GridTie.AcDc.MaxDcLinkVoltage)
+ {
+ _curtailFlag = false;
+ upperVoltage = configFile.GridTie.AcDc.MaxDcLinkVoltage;
+ upperVoltage.WriteLine(" New Upper limit");
+ Console.WriteLine("Upper Voltage decreased: Smaller than the default value, value clamped");
+ }
+ else
+ {
+ Console.WriteLine("Upper Voltage decreased: New Upper limit");
+ upperVoltage.WriteLine(" New Upper limit");
+ }
+ }
+ else
+ {
+ deadBand.WriteLine("W :We are in Dead band area");
+ upperVoltage.WriteLine(" same Upper limit from last cycle");
+ }
+ }
+ else
+ {
+ Console.WriteLine("Curtail Flag is false , no need to curtail");
+ upperVoltage.WriteLine(" same Upper limit from last cycle");
+ }
+ }
+ inverters.ForEach(d => d.Control.Dc.MaxVoltage = upperVoltage);
+ Console.WriteLine(" ****************** ");
+ }
+
+ // why this is not in Controller?
+ private static void DistributePower(StatusRecord record, EssControl essControl)
+ {
+ var nInverters = record.AcDc.Devices.Count;
+
+ var powerPerInverterPhase = nInverters > 0
+ ? essControl.PowerSetpoint / nInverters / 3
+ : 0;
+
+ record.AcDc.Devices.ForEach(d =>
+ {
+ d.Control.Ac.PhaseControl = PhaseControl.Asymmetric;
+ d.Control.Ac.Power.L1 = powerPerInverterPhase;
+ d.Control.Ac.Power.L2 = powerPerInverterPhase;
+ d.Control.Ac.Power.L3 = powerPerInverterPhase;
+ });
+ }
+
+ // To test, most probably the curtailing flag will not work
+ private static void PerformLed(this StatusRecord record)
+ {
+ if (record.StateMachine.State == 23)
+ {
+ switch (record.EssControl.Mode)
+ {
+ case EssMode.CalibrationCharge:
+ record.Relays?.PerformSlowFlashingGreenLed();
+ break;
+ case EssMode.OptimizeSelfConsumption when !_curtailFlag:
+ record.Relays?.PerformSolidGreenLed();
+ break;
+ case EssMode.Off:
+ break;
+ case EssMode.OffGrid:
+ break;
+ case EssMode.HeatBatteries:
+ break;
+ case EssMode.ReachMinSoc:
+ break;
+ case EssMode.NoGridMeter:
+ break;
+ default:
+ {
+ if (_curtailFlag)
+ {
+ record.Relays?.PerformFastFlashingGreenLed();
+ }
+
+ break;
+ }
+ }
+ }
+ else if (record.Battery?.Soc != null && record.StateMachine.State != 23 && record.Battery.Soc > 50)
+ {
+ record.Relays?.PerformSolidOrangeLed();
+ }
+ else if (record.Battery?.Soc != null && record.StateMachine.State != 23 && record.Battery.Soc < 50 && record.Battery.Soc > 20)
+ {
+ record.Relays?.PerformSlowFlashingOrangeLed();
+ }
+ else if (record.Battery?.Soc != null && record.StateMachine.State != 23 && record.Battery.Soc < 20)
+ {
+ record.Relays?.PerformFastFlashingOrangeLed();
+ }
+
+ var criticalAlarm = record.DetectAlarmStates();
+
+ if (criticalAlarm is not null)
+ {
+ record.Relays?.PerformFastFlashingRedLed();
+ }
+ }
+
+ private static Double IncreaseInverterUpperLimit(Double upperLimit, Double stepSize)
+ {
+ return upperLimit + stepSize;
+ }
+
+ private static Double DecreaseInverterUpperLimit(Double upperLimit, Double stepSize)
+ {
+ return upperLimit - stepSize;
+ }
+
+ private static UInt16 ClampStepSize(UInt16 stepSize)
+ {
+ return stepSize switch
+ {
+ > 5 => 5,
+ <= 1 => 1,
+ _ => stepSize
+ };
+ }
+
+ private static void ApplyAcDcDefaultSettings(this SystemControlRegisters? sc)
+ {
+ if (sc is null)
+ return;
+
+ sc.ReferenceFrame = ReferenceFrame.Consumer;
+ sc.SystemConfig = AcDcAndDcDc;
+
+ #if DEBUG
+ sc.CommunicationTimeout = TimeSpan.FromMinutes(2);
+ #else
+ sc.CommunicationTimeout = TimeSpan.FromSeconds(20);
+ #endif
+
+ sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
+ sc.UseSlaveIdForAddressing = true;
+ sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
+ sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
+
+ sc.ResetAlarmsAndWarnings = true;
+ }
+
+ private static void ApplyDcDcDefaultSettings(this SystemControlRegisters? sc)
+ {
+
+ if (sc is null)
+ return;
+
+ sc.SystemConfig = DcDcOnly;
+ #if DEBUG
+ sc.CommunicationTimeout = TimeSpan.FromMinutes(2);
+ #else
+ sc.CommunicationTimeout = TimeSpan.FromSeconds(20);
+ #endif
+
+ sc.PowerSetPointActivation = PowerSetPointActivation.Immediate;
+ sc.UseSlaveIdForAddressing = true;
+ sc.SlaveErrorHandling = SlaveErrorHandling.Relaxed;
+ sc.SubSlaveErrorHandling = SubSlaveErrorHandling.Off;
+
+ sc.ResetAlarmsAndWarnings = true;
+ }
+
+ private static async Task UploadCsv(StatusRecord status, DateTime timeStamp)
+ {
+
+ var csv = status.ToCsv().LogInfo();
+
+ await RestApiSavingFile(csv);
+
+ var s3Config = status.Config.S3;
+
+ if (s3Config is null)
+ return false;
+
+ //Concatenating 15 files in one file
+ return await ConcatinatingAndCompressingFiles(timeStamp, s3Config);
+ }
+
+ private static async Task ConcatinatingAndCompressingFiles(DateTime timeStamp, S3Config s3Config)
+ {
+ if (_counterOfFile >= NbrOfFileToConcatenate)
+ {
+ _counterOfFile = 0;
+
+ var logFileConcatenator = new LogFileConcatenator();
+
+ var s3Path = timeStamp.ToUnixTime() + ".csv";
+ s3Path.WriteLine("");
+ var csvToSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate);
+
+ var request = s3Config.CreatePutRequest(s3Path);
+
+ //Use this for no compression
+ //var response = await request.PutAsync(new StringContent(csv));
+ var compressedBytes = CompresseBytes(csvToSend);
+
+ // Encode the compressed byte array as a Base64 string
+ string base64String = Convert.ToBase64String(compressedBytes);
+
+ // Create StringContent from Base64 string
+ var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
+
+ // Upload the compressed data (ZIP archive) to S3
+ var response = await request.PutAsync(stringContent);
+
+ if (response.StatusCode != 200)
+ {
+ Console.WriteLine("ERROR: PUT");
+ var error = await response.GetStringAsync();
+ Console.WriteLine(error);
+ Heartbit(new DateTime(0));
+ return false;
+ }
+
+ Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
+
+ Heartbit(timeStamp);
+ }
+ _counterOfFile++;
+
+ return true;
+ }
+
+ private static void Heartbit(DateTime timeStamp)
+ {
+ var s3Bucket = Config.Load().S3?.Bucket;
+ var tryParse = TryParse(s3Bucket?.Split("-")[0], out var installationId);
+ var parse = TryParse(timeStamp.ToUnixTime().ToString(), out var nameOfCsvFile);
+
+ if (tryParse)
+ {
+ var returnedStatus = new StatusMessage
+ {
+ InstallationId = installationId,
+ Product = 0, // Salimax is always 0
+ Status = _salimaxAlarmState,
+ Type = MessageType.Heartbit,
+ Timestamp = nameOfCsvFile
+ };
+ if (s3Bucket != null)
+ RabbitMqManager.InformMiddleware(returnedStatus);
+ }
+ }
+
+ private static Byte[] CompresseBytes(String csvToSend)
+ {
+ //Compress CSV data to a byte array
+ Byte[] compressedBytes;
+ using (var memoryStream = new MemoryStream())
+ {
+ //Create a zip directory and put the compressed file inside
+ using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
+ {
+ var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
+ using (var entryStream = entry.Open())
+ using (var writer = new StreamWriter(entryStream))
+ {
+ writer.Write(csvToSend);
+ }
+ }
+
+ compressedBytes = memoryStream.ToArray();
+ }
+
+ return compressedBytes;
+ }
+
+ private static async Task RestApiSavingFile(String csv)
+ {
+ // This is for the Rest API
+ // Check if the directory exists, and create it if it doesn't
+ const String directoryPath = "/var/www/html";
+
+ if (!Directory.Exists(directoryPath))
+ {
+ Directory.CreateDirectory(directoryPath);
+ }
+
+ string filePath = Path.Combine(directoryPath, "status.csv");
+
+ await File.WriteAllTextAsync(filePath, csv.SplitLines().Where(l => !l.Contains("Secret")).JoinLines());
+ }
+
+ private static Boolean IsPowerOfTwo(Int32 n)
+ {
+ return n > 0 && (n & (n - 1)) == 0;
+ }
+
+ private static void ApplyConfigFile(this StatusRecord status, Configuration? config)
+ {
+ if (config == null) return;
+
+ status.Config.MinSoc = config.MinimumSoC;
+ status.Config.GridSetPoint = config.GridSetPoint * 1000; // converted from kW to W
+ // status.Config.ForceCalibrationChargeState = config.CalibrationChargeState;
+ //
+ // if (config.CalibrationChargeState == CalibrationChargeType.RepetitivelyEvery)
+ // {
+ // status.Config.DayAndTimeForRepetitiveCalibration = config.CalibrationChargeDate;
+ // }
+ // else if (config.CalibrationChargeState == CalibrationChargeType.AdditionallyOnce)
+ // {
+ // status.Config.DayAndTimeForAdditionalCalibration = config.CalibrationChargeDate;
+ // }
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/S3Config.cs b/csharp/App/SodiStoreMax/src/S3Config.cs
new file mode 100644
index 000000000..407e93330
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/S3Config.cs
@@ -0,0 +1,79 @@
+using System.Security.Cryptography;
+using Flurl;
+using Flurl.Http;
+using InnovEnergy.Lib.Utils;
+using static System.Text.Encoding;
+using Convert = System.Convert;
+
+namespace InnovEnergy.App.SodiStoreMax;
+
+public record S3Config
+{
+ public required String Bucket { get; init; }
+ public required String Region { get; init; }
+ public required String Provider { get; init; }
+ public required String Key { get; init; }
+ public required String Secret { get; init; }
+ public required String ContentType { get; init; }
+
+ public String Host => $"{Bucket}.{Region}.{Provider}";
+ public String Url => $"https://{Host}";
+
+ public IFlurlRequest CreatePutRequest(String s3Path) => CreateRequest("PUT", s3Path);
+ public IFlurlRequest CreateGetRequest(String s3Path) => CreateRequest("GET", s3Path);
+
+ private IFlurlRequest CreateRequest(String method, String s3Path)
+ {
+ var date = DateTime.UtcNow.ToString("r");
+ var auth = CreateAuthorization(method, s3Path, date);
+
+ return Url
+ .AppendPathSegment(s3Path)
+ .WithHeader("Host", Host)
+ .WithHeader("Date", date)
+ .WithHeader("Authorization", auth)
+ .AllowAnyHttpStatus();
+ }
+
+ private String CreateAuthorization(String method,
+ String s3Path,
+ String date)
+ {
+ return CreateAuthorization
+ (
+ method : method,
+ bucket : Bucket,
+ s3Path : s3Path,
+ date : date,
+ s3Key : Key,
+ s3Secret : Secret,
+ contentType: ContentType
+ );
+ }
+
+
+
+ private static String CreateAuthorization(String method,
+ String bucket,
+ String s3Path,
+ String date,
+ String s3Key,
+ String s3Secret,
+ String contentType = "application/base64",
+ String md5Hash = "")
+ {
+
+ contentType = "application/base64; charset=utf-8";
+ //contentType = "text/plain; charset=utf-8"; //this to use when sending plain csv to S3
+
+ var payload = $"{method}\n{md5Hash}\n{contentType}\n{date}\n/{bucket.Trim('/')}/{s3Path.Trim('/')}";
+ using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret));
+
+ var signature = UTF8
+ .GetBytes(payload)
+ .Apply(hmacSha1.ComputeHash)
+ .Apply(Convert.ToBase64String);
+
+ return $"AWS {s3Key}:{signature}";
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SaliMaxRelays/CombinedAdamRelaysRecord.cs b/csharp/App/SodiStoreMax/src/SaliMaxRelays/CombinedAdamRelaysRecord.cs
new file mode 100644
index 000000000..d126b92ea
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SaliMaxRelays/CombinedAdamRelaysRecord.cs
@@ -0,0 +1,211 @@
+using System.Reflection.Metadata;
+
+namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+#pragma warning disable CS8602 // Dereference of a possibly null reference.
+
+public class CombinedAdamRelaysRecord : IRelaysRecord
+{
+ private const UInt16 SlowFreq = 3000;
+ private const UInt16 HighFreq = 1000;
+
+ public CombinedAdamRelaysRecord(RelaysRecordAdam6060? relaysRecordAdam6060, RelaysRecordAdam6360D? relaysRecordAdam6360D)
+ {
+ _recordAdam6060 = relaysRecordAdam6060;
+ _recordAdam6360D = relaysRecordAdam6360D;
+ }
+
+ private static RelaysRecordAdam6060? _recordAdam6060;
+ private static RelaysRecordAdam6360D? _recordAdam6360D;
+
+ public static IRelaysRecord Instance { get; } = new CombinedAdamRelaysRecord(_recordAdam6060, _recordAdam6360D);
+
+
+ public Boolean K1GridBusIsConnectedToGrid => _recordAdam6360D.K1GridBusIsConnectedToGrid;
+
+ public Boolean K2IslandBusIsConnectedToGridBus => _recordAdam6360D.K2IslandBusIsConnectedToGridBus;
+ public Boolean FiWarning => _recordAdam6360D.FiWarning;
+ public Boolean FiError => _recordAdam6360D.FiError;
+ public Boolean K2ConnectIslandBusToGridBus
+ {
+ get => _recordAdam6360D.K2ConnectIslandBusToGridBus;
+ set => _recordAdam6360D.K2ConnectIslandBusToGridBus = value;
+ }
+
+ public Boolean Inverter1WagoStatus => _recordAdam6360D.Inverter1WagoStatus;
+ public Boolean Inverter2WagoStatus => _recordAdam6360D.Inverter2WagoStatus;
+ public Boolean Inverter3WagoStatus => _recordAdam6360D.Inverter3WagoStatus;
+ public Boolean Inverter4WagoStatus => _recordAdam6360D.Inverter4WagoStatus;
+
+ public Boolean Dc1WagoStatus => _recordAdam6060.Dc1WagoStatus;
+ public Boolean Dc2WagoStatus => _recordAdam6060.Dc2WagoStatus;
+ public Boolean Dc3WagoStatus => _recordAdam6060.Dc3WagoStatus;
+ public Boolean Dc4WagoStatus => _recordAdam6060.Dc4WagoStatus;
+ public Boolean DcSystemControlWagoStatus => _recordAdam6060.DcSystemControlWagoStatus;
+
+ public Boolean LedGreen { get => _recordAdam6360D.LedGreen; set => _recordAdam6360D.LedGreen = value;}
+ public Boolean LedRed { get => _recordAdam6360D.LedRed; set => _recordAdam6360D.LedRed = value;}
+ public Boolean Harvester1Step => _recordAdam6360D.Harvester1Step;
+ public Boolean Harvester2Step => _recordAdam6360D.Harvester2Step;
+ public Boolean Harvester3Step => _recordAdam6360D.Harvester3Step;
+ public Boolean Harvester4Step => _recordAdam6360D.Harvester4Step;
+
+ public UInt16 DigitalOutput0Mode { get => _recordAdam6360D.DigitalOutput0Mode; set => _recordAdam6360D.DigitalOutput0Mode = value; }
+
+ public UInt16 DigitalOutput1Mode
+ {
+ get => _recordAdam6360D.DigitalOutput1Mode;
+ set => _recordAdam6360D.DigitalOutput1Mode = value;
+ }
+
+ public UInt16 DigitalOutput2Mode
+ {
+ get => _recordAdam6360D.DigitalOutput2Mode;
+ set => _recordAdam6360D.DigitalOutput2Mode = value;
+ }
+
+ public UInt16 DigitalOutput3Mode
+ {
+ get => _recordAdam6360D.DigitalOutput3Mode;
+ set => _recordAdam6360D.DigitalOutput3Mode = value;
+ }
+
+ public UInt16 DigitalOutput4Mode
+ {
+ get => _recordAdam6360D.DigitalOutput4Mode;
+ set => _recordAdam6360D.DigitalOutput4Mode = value;
+ }
+
+ public UInt16 DigitalOutput5Mode
+ {
+ get => _recordAdam6360D.DigitalOutput5Mode;
+ set => _recordAdam6360D.DigitalOutput5Mode = value;
+ }
+
+ public Boolean Do0StartPulse { get => _recordAdam6360D.Do0Pulse; set => _recordAdam6360D.Do0Pulse = value; }
+ public Boolean Do1StartPulse { get => _recordAdam6360D.Do1Pulse; set => _recordAdam6360D.Do1Pulse = value; }
+ public Boolean Do2StartPulse { get => _recordAdam6360D.Do2Pulse; set => _recordAdam6360D.Do2Pulse = value; }
+ public Boolean Do3StartPulse { get => _recordAdam6360D.Do3Pulse; set => _recordAdam6360D.Do3Pulse = value; }
+ public Boolean Do4StartPulse { get => _recordAdam6360D.Do4Pulse; set => _recordAdam6360D.Do4Pulse = value; }
+ public Boolean Do5StartPulse { get => _recordAdam6360D.Do5Pulse; set => _recordAdam6360D.Do5Pulse = value; }
+
+
+ public UInt16 PulseOut0LowTime { get => _recordAdam6360D.PulseOut0LowTime; set => _recordAdam6360D.PulseOut0LowTime = value; }
+ public UInt16 PulseOut1LowTime { get => _recordAdam6360D.PulseOut1LowTime; set => _recordAdam6360D.PulseOut1LowTime = value; }
+ public UInt16 PulseOut2LowTime { get => _recordAdam6360D.PulseOut2LowTime; set => _recordAdam6360D.PulseOut2LowTime = value; }
+ public UInt16 PulseOut3LowTime { get => _recordAdam6360D.PulseOut3LowTime; set => _recordAdam6360D.PulseOut3LowTime = value; }
+ public UInt16 PulseOut4LowTime { get => _recordAdam6360D.PulseOut4LowTime; set => _recordAdam6360D.PulseOut4LowTime = value; }
+ public UInt16 PulseOut5LowTime { get => _recordAdam6360D.PulseOut5LowTime; set => _recordAdam6360D.PulseOut5LowTime = value; }
+
+ public UInt16 PulseOut0HighTime { get => _recordAdam6360D.PulseOut0HighTime; set => _recordAdam6360D.PulseOut0HighTime = value; }
+ public UInt16 PulseOut1HighTime { get => _recordAdam6360D.PulseOut1HighTime; set => _recordAdam6360D.PulseOut1HighTime = value; }
+ public UInt16 PulseOut2HighTime { get => _recordAdam6360D.PulseOut2HighTime; set => _recordAdam6360D.PulseOut2HighTime = value; }
+ public UInt16 PulseOut3HighTime { get => _recordAdam6360D.PulseOut3HighTime; set => _recordAdam6360D.PulseOut3HighTime = value; }
+ public UInt16 PulseOut4HighTime { get => _recordAdam6360D.PulseOut4HighTime; set => _recordAdam6360D.PulseOut4HighTime = value; }
+ public UInt16 PulseOut5HighTime { get => _recordAdam6360D.PulseOut5HighTime; set => _recordAdam6360D.PulseOut5HighTime = value; }
+
+ /**************************** Green LED *********************************/
+
+ public void PerformSolidGreenLed()
+ {
+ DigitalOutput0Mode = 0;
+ DigitalOutput1Mode = 0;
+ LedGreen = true;
+ LedRed = false;
+ }
+
+ public void PerformSlowFlashingGreenLed()
+ {
+ PulseOut0HighTime = SlowFreq;
+ PulseOut0LowTime = SlowFreq;
+ DigitalOutput0Mode = 2;
+ Do0StartPulse = true;
+ Do1StartPulse = false; // make sure the red LED is off
+
+ Console.WriteLine("Green Slow Flashing Starting");
+ }
+
+ public void PerformFastFlashingGreenLed()
+ {
+ PulseOut0HighTime = HighFreq;
+ PulseOut0LowTime = HighFreq;
+ DigitalOutput0Mode = 2;
+ Do0StartPulse = true;
+ Do1StartPulse = false;// make sure the red LED is off
+
+ Console.WriteLine("Green Slow Flashing Starting");
+ }
+
+ /**************************** Orange LED *********************************/
+
+ public void PerformSolidOrangeLed()
+ {
+ DigitalOutput0Mode = 0;
+ DigitalOutput1Mode = 0;
+ LedGreen = true;
+ LedRed = true;
+ }
+
+ public void PerformSlowFlashingOrangeLed()
+ {
+ PerformSlowFlashingGreenLed();
+ PerformSlowFlashingRedLed();
+ Do0StartPulse = true;
+ Do1StartPulse = true;
+
+ Console.WriteLine("Orange Slow Flashing Starting");
+ }
+
+ public void PerformFastFlashingOrangeLed()
+ {
+ PerformFastFlashingGreenLed();
+ PerformFastFlashingRedLed();
+ Do0StartPulse = true;
+ Do1StartPulse = true;
+ Console.WriteLine("Orange Fast Flashing Starting");
+ }
+
+ /**************************** RED LED *********************************/
+
+ public void PerformSolidRedLed()
+ {
+ DigitalOutput0Mode = 0;
+ DigitalOutput1Mode = 0;
+ LedGreen = false;
+ LedRed = true;
+ }
+
+ public void PerformSlowFlashingRedLed()
+ {
+ PulseOut1HighTime = SlowFreq;
+ PulseOut1LowTime = SlowFreq;
+ DigitalOutput1Mode = 2;
+ Do0StartPulse = false; // make sure the green LED is off
+ Do1StartPulse = true;
+
+ Console.WriteLine("Red Slow Flashing Starting");
+ }
+
+ public void PerformFastFlashingRedLed()
+ {
+ PulseOut1HighTime = HighFreq;
+ PulseOut1LowTime = HighFreq;
+ DigitalOutput1Mode = 2;
+ Do0StartPulse = false; // make sure the green LED is off
+ Do1StartPulse = true;
+
+ Console.WriteLine("Red Fast Flashing Starting");
+ }
+
+ public RelaysRecordAdam6360D? GetAdam6360DRecord()
+ {
+ return _recordAdam6360D;
+ }
+
+ public RelaysRecordAdam6060? GetAdam6060Record()
+ {
+ return _recordAdam6060;
+ }
+
+ public IEnumerable K3InverterIsConnectedToIslandBus => _recordAdam6360D.K3InverterIsConnectedToIslandBus;
+
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SaliMaxRelays/IRelaysRecord.cs b/csharp/App/SodiStoreMax/src/SaliMaxRelays/IRelaysRecord.cs
new file mode 100644
index 000000000..a971f06dc
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SaliMaxRelays/IRelaysRecord.cs
@@ -0,0 +1,78 @@
+namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+
+
+public interface IRelaysRecord
+{
+ Boolean K1GridBusIsConnectedToGrid { get; }
+ Boolean K2IslandBusIsConnectedToGridBus { get; }
+ IEnumerable K3InverterIsConnectedToIslandBus { get; }
+ Boolean FiWarning { get; }
+ Boolean FiError { get; }
+ Boolean K2ConnectIslandBusToGridBus { get; set; }
+
+ // Boolean Inverter1WagoRelay { get; set; } // to add in the future
+ // Boolean Inverter2WagoRelay { get; set; } // to add in the future
+ // Boolean Inverter3WagoRelay { get; set; } // to add in the future
+ // Boolean Inverter4WagoRelay { get; set; } // to add in the future
+
+ Boolean Inverter1WagoStatus { get; }
+ Boolean Inverter2WagoStatus { get; }
+ Boolean Inverter3WagoStatus { get; }
+ Boolean Inverter4WagoStatus { get; }
+
+ Boolean Dc1WagoStatus { get; } // to test
+ Boolean Dc2WagoStatus { get; } // to test
+ Boolean Dc3WagoStatus { get; } // to test
+ Boolean Dc4WagoStatus { get; } // to test
+
+ Boolean DcSystemControlWagoStatus { get; } // to test
+
+ Boolean LedGreen { get; set; }
+ Boolean LedRed { get; }
+ Boolean Harvester1Step { get; }
+ Boolean Harvester2Step { get; }
+ Boolean Harvester3Step { get; }
+ Boolean Harvester4Step { get; }
+
+ Boolean Do0StartPulse { get; set; }
+ Boolean Do1StartPulse { get; set; }
+ Boolean Do2StartPulse { get; set; }
+ Boolean Do3StartPulse { get; set; }
+ Boolean Do4StartPulse { get; set; }
+ Boolean Do5StartPulse { get; set; }
+
+ UInt16 DigitalOutput0Mode { get; set; }
+ UInt16 DigitalOutput1Mode { get; set; }
+ UInt16 DigitalOutput2Mode { get; set; }
+ UInt16 DigitalOutput3Mode { get; set; }
+ UInt16 DigitalOutput4Mode { get; set; }
+ UInt16 DigitalOutput5Mode { get; set; }
+
+ UInt16 PulseOut0LowTime { get; set; }
+ UInt16 PulseOut1LowTime { get; set; }
+ UInt16 PulseOut2LowTime { get; set; }
+ UInt16 PulseOut3LowTime { get; set; }
+ UInt16 PulseOut4LowTime { get; set; }
+ UInt16 PulseOut5LowTime { get; set; }
+
+ UInt16 PulseOut0HighTime { get; set; }
+ UInt16 PulseOut1HighTime { get; set; }
+ UInt16 PulseOut2HighTime { get; set; }
+ UInt16 PulseOut3HighTime { get; set; }
+ UInt16 PulseOut4HighTime { get; set; }
+ UInt16 PulseOut5HighTime { get; set; }
+
+ void PerformSolidGreenLed();
+ void PerformSlowFlashingGreenLed();
+ void PerformFastFlashingGreenLed();
+
+
+ void PerformSolidOrangeLed();
+ void PerformSlowFlashingOrangeLed();
+ void PerformFastFlashingOrangeLed();
+
+ void PerformSolidRedLed();
+ void PerformSlowFlashingRedLed();
+ void PerformFastFlashingRedLed();
+
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysDeviceADAM6360.cs b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysDeviceADAM6360.cs
new file mode 100644
index 000000000..4a47e914c
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysDeviceADAM6360.cs
@@ -0,0 +1,40 @@
+using InnovEnergy.Lib.Devices.Adam6360D;
+using InnovEnergy.Lib.Protocols.Modbus.Channels;
+
+namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+
+public class RelaysDeviceAdam6360
+{
+ private Adam6360DDevice AdamDevice6360D { get; }
+
+ public RelaysDeviceAdam6360(String hostname) => AdamDevice6360D = new Adam6360DDevice(hostname, 2);
+ public RelaysDeviceAdam6360(Channel channel) => AdamDevice6360D = new Adam6360DDevice(channel, 2);
+
+
+ public RelaysRecordAdam6360D? Read()
+ {
+ try
+ {
+ return AdamDevice6360D.Read();
+ }
+ catch (Exception e)
+ {
+ $"Failed to read from {nameof(RelaysDeviceAdam6360)}\n{e}".LogError();
+ return null;
+ }
+ }
+
+ public void Write(RelaysRecordAdam6360D r)
+ {
+ try
+ {
+ AdamDevice6360D.Write(r);
+ }
+ catch (Exception e)
+ {
+ $"Failed to write to {nameof(RelaysDeviceAdam6360)}\n{e}".LogError();
+ }
+ }
+}
+
+
diff --git a/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysDeviceAdam6060.cs b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysDeviceAdam6060.cs
new file mode 100644
index 000000000..c53175c24
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysDeviceAdam6060.cs
@@ -0,0 +1,38 @@
+using InnovEnergy.Lib.Devices.Adam6060;
+using InnovEnergy.Lib.Protocols.Modbus.Channels;
+
+namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+
+public class RelaysDeviceAdam6060
+{
+ private Adam6060Device AdamDevice6060 { get; }
+
+ public RelaysDeviceAdam6060(String hostname) => AdamDevice6060 = new Adam6060Device(hostname, 2);
+ public RelaysDeviceAdam6060(Channel channel) => AdamDevice6060 = new Adam6060Device(channel, 2);
+
+
+ public RelaysRecordAdam6060? Read()
+ {
+ try
+ {
+ return AdamDevice6060.Read();
+ }
+ catch (Exception e)
+ {
+ $"Failed to read from {nameof(RelaysDeviceAdam6060)}\n{e}".LogError();
+ return null;
+ }
+ }
+
+ public void Write(RelaysRecordAdam6060 r)
+ {
+ try
+ {
+ AdamDevice6060.Write(r);
+ }
+ catch (Exception e)
+ {
+ $"Failed to write to {nameof(RelaysDeviceAdam6060)}\n{e}".LogError();
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysDeviceAmax.cs b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysDeviceAmax.cs
new file mode 100644
index 000000000..c5cbbd010
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysDeviceAmax.cs
@@ -0,0 +1,37 @@
+using InnovEnergy.Lib.Devices.Amax5070;
+using InnovEnergy.Lib.Protocols.Modbus.Channels;
+
+
+namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+
+public class RelaysDeviceAmax
+{
+ private Amax5070Device AmaxDevice { get; }
+
+ public RelaysDeviceAmax(Channel channel) => AmaxDevice = new Amax5070Device(channel);
+
+ public RelaysRecordAmax? Read()
+ {
+ try
+ {
+ return AmaxDevice.Read();
+ }
+ catch (Exception e)
+ {
+ $"Failed to read from {nameof(RelaysDeviceAmax)}\n{e}".LogError();
+ return null;
+ }
+ }
+
+ public void Write(RelaysRecordAmax r)
+ {
+ try
+ {
+ AmaxDevice.Write(r);
+ }
+ catch (Exception e)
+ {
+ $"Failed to write to {nameof(RelaysDeviceAmax)}\n{e}".LogError();
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysRecordAdam6060.cs b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysRecordAdam6060.cs
new file mode 100644
index 000000000..50c40f251
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysRecordAdam6060.cs
@@ -0,0 +1,24 @@
+using InnovEnergy.Lib.Devices.Adam6060;
+
+
+namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+
+public class RelaysRecordAdam6060
+{
+ private readonly Adam6060Registers _Regs;
+
+ private RelaysRecordAdam6060(Adam6060Registers regs) => _Regs = regs;
+
+
+ public Boolean Dc1WagoStatus => _Regs.DigitalInput0; // to test
+ public Boolean Dc2WagoStatus => _Regs.DigitalInput1; // to test
+ public Boolean Dc3WagoStatus => _Regs.DigitalInput4; // to test
+ public Boolean Dc4WagoStatus => _Regs.DigitalInput5; // to test
+
+ public Boolean DcSystemControlWagoStatus => _Regs.DigitalInput3; // to test
+
+
+ public static implicit operator Adam6060Registers(RelaysRecordAdam6060 d) => d._Regs;
+ public static implicit operator RelaysRecordAdam6060(Adam6060Registers d) => new RelaysRecordAdam6060(d);
+
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysRecordAdam6360D.cs b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysRecordAdam6360D.cs
new file mode 100644
index 000000000..2e87b78f2
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysRecordAdam6360D.cs
@@ -0,0 +1,81 @@
+using InnovEnergy.Lib.Devices.Adam6360D;
+
+namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+
+public class RelaysRecordAdam6360D
+{
+ private readonly Adam6360DRegisters _Regs;
+
+ private RelaysRecordAdam6360D(Adam6360DRegisters regs) => _Regs = regs;
+
+ public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput6;
+ public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput4;
+
+ public Boolean Inverter1WagoStatus => _Regs.DigitalInput8;
+ public Boolean Inverter2WagoStatus => _Regs.DigitalInput9;
+ public Boolean Inverter3WagoStatus => _Regs.DigitalInput10;
+ public Boolean Inverter4WagoStatus => _Regs.DigitalInput11;
+
+ public IEnumerable K3InverterIsConnectedToIslandBus
+ {
+ get
+ {
+ yield return K3Inverter1IsConnectedToIslandBus;
+ yield return K3Inverter2IsConnectedToIslandBus;
+ yield return K3Inverter3IsConnectedToIslandBus;
+ yield return K3Inverter4IsConnectedToIslandBus;
+ }
+ }
+
+ private Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput0; // change it to private should be ok
+ private Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput1;
+ private Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput2;
+ private Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput3;
+
+ public Boolean FiWarning => !_Regs.DigitalInput5;
+ public Boolean FiError => !_Regs.DigitalInput7;
+
+ public Boolean Harvester1Step =>_Regs.DigitalOutput2;
+ public Boolean Harvester2Step =>_Regs.DigitalOutput3;
+ public Boolean Harvester3Step =>_Regs.DigitalOutput4;
+ public Boolean Harvester4Step =>_Regs.DigitalOutput5;
+
+ public Boolean LedGreen { get =>_Regs.DigitalOutput0; set => _Regs.DigitalOutput0 = value;}
+ public Boolean LedRed { get =>_Regs.DigitalOutput1; set => _Regs.DigitalOutput1 = value;}
+
+ public Boolean Do0Pulse { get => _Regs.Do0Pulse; set => _Regs.Do0Pulse = value;}
+ public Boolean Do1Pulse { get => _Regs.Do1Pulse; set => _Regs.Do1Pulse = value;}
+ public Boolean Do2Pulse { get => _Regs.Do2Pulse; set => _Regs.Do2Pulse = value;}
+ public Boolean Do3Pulse { get => _Regs.Do3Pulse; set => _Regs.Do3Pulse = value;}
+ public Boolean Do4Pulse { get => _Regs.Do4Pulse; set => _Regs.Do4Pulse = value;}
+ public Boolean Do5Pulse { get => _Regs.Do5Pulse; set => _Regs.Do5Pulse = value;}
+
+ public UInt16 PulseOut0LowTime { get => _Regs.PulseOut0LowTime; set => _Regs.PulseOut0LowTime = value;} //in milleseconds
+ public UInt16 PulseOut1LowTime { get => _Regs.PulseOut1LowTime; set => _Regs.PulseOut1LowTime = value;}
+ public UInt16 PulseOut2LowTime { get => _Regs.PulseOut2LowTime; set => _Regs.PulseOut2LowTime = value;}
+ public UInt16 PulseOut3LowTime { get => _Regs.PulseOut3LowTime; set => _Regs.PulseOut3LowTime = value;}
+ public UInt16 PulseOut4LowTime { get => _Regs.PulseOut4LowTime; set => _Regs.PulseOut4LowTime = value;}
+ public UInt16 PulseOut5LowTime { get => _Regs.PulseOut5LowTime; set => _Regs.PulseOut5LowTime = value;}
+
+ public UInt16 PulseOut0HighTime { get => _Regs.PulseOut0HighTime; set => _Regs.PulseOut0HighTime = value;} // in milleseconds
+ public UInt16 PulseOut1HighTime { get => _Regs.PulseOut1HighTime; set => _Regs.PulseOut1HighTime = value;}
+ public UInt16 PulseOut2HighTime { get => _Regs.PulseOut2HighTime; set => _Regs.PulseOut2HighTime = value;}
+ public UInt16 PulseOut3HighTime { get => _Regs.PulseOut3HighTime; set => _Regs.PulseOut3HighTime = value;}
+ public UInt16 PulseOut4HighTime { get => _Regs.PulseOut4HighTime; set => _Regs.PulseOut4HighTime = value;}
+ public UInt16 PulseOut5HighTime { get => _Regs.PulseOut5HighTime; set => _Regs.PulseOut5HighTime = value;}
+
+ public UInt16 DigitalOutput0Mode { get => _Regs.DigitalOutput0Mode; set => _Regs.DigitalOutput0Mode = value;} // To test: 0, 1 or 2
+ public UInt16 DigitalOutput1Mode { get => _Regs.DigitalOutput1Mode; set => _Regs.DigitalOutput1Mode = value;}
+ public UInt16 DigitalOutput2Mode { get => _Regs.DigitalOutput2Mode; set => _Regs.DigitalOutput2Mode = value;}
+ public UInt16 DigitalOutput3Mode { get => _Regs.DigitalOutput3Mode; set => _Regs.DigitalOutput3Mode = value;}
+ public UInt16 DigitalOutput4Mode { get => _Regs.DigitalOutput4Mode; set => _Regs.DigitalOutput4Mode = value;}
+ public UInt16 DigitalOutput5Mode { get => _Regs.DigitalOutput5Mode; set => _Regs.DigitalOutput5Mode = value;}
+
+ public Boolean K2ConnectIslandBusToGridBus { get => _Regs.Relay0; set => _Regs.Relay0 = value;}
+
+ public static implicit operator Adam6360DRegisters(RelaysRecordAdam6360D d) => d._Regs;
+ public static implicit operator RelaysRecordAdam6360D(Adam6360DRegisters d) => new RelaysRecordAdam6360D(d);
+
+}
+
+
diff --git a/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysRecordAmax.cs b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysRecordAmax.cs
new file mode 100644
index 000000000..7ee522c49
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SaliMaxRelays/RelaysRecordAmax.cs
@@ -0,0 +1,134 @@
+using InnovEnergy.Lib.Devices.Amax5070;
+
+namespace InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+
+public class RelaysRecordAmax : IRelaysRecord
+{
+ private readonly Amax5070Registers _Regs;
+
+ private RelaysRecordAmax(Amax5070Registers regs) => _Regs = regs;
+
+ public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput22;
+ public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput20;
+
+ public Boolean Inverter1WagoStatus => _Regs.DigitalInput0;
+ public Boolean Inverter2WagoStatus => _Regs.DigitalInput1;
+ public Boolean Inverter3WagoStatus => _Regs.DigitalInput2;
+ public Boolean Inverter4WagoStatus => _Regs.DigitalInput3;
+
+ public Boolean Dc1WagoStatus => _Regs.DigitalInput6;
+ public Boolean Dc2WagoStatus => _Regs.DigitalInput7;
+ public Boolean Dc3WagoStatus => _Regs.DigitalInput10;
+ public Boolean Dc4WagoStatus => _Regs.DigitalInput11;
+ public Boolean DcSystemControlWagoStatus => _Regs.DigitalInput9;
+
+ public Boolean LedGreen
+ {
+ get => _Regs.DigitalOutput0;
+ set => _Regs.DigitalOutput0 = value;
+ }
+
+ public Boolean LedRed => _Regs.DigitalOutput1;
+ public Boolean Harvester1Step => _Regs.DigitalOutput2;
+ public Boolean Harvester2Step => _Regs.DigitalOutput3;
+ public Boolean Harvester3Step => _Regs.DigitalOutput4;
+ public Boolean Harvester4Step => _Regs.DigitalOutput5;
+ public Boolean Do0StartPulse { get; set; }
+ public Boolean Do1StartPulse { get; set; }
+ public Boolean Do2StartPulse { get; set; }
+ public Boolean Do3StartPulse { get; set; }
+ public Boolean Do4StartPulse { get; set; }
+ public Boolean Do5StartPulse { get; set; }
+ public UInt16 DigitalOutput0Mode { get; set; }
+ public UInt16 DigitalOutput1Mode { get; set; }
+ public UInt16 DigitalOutput2Mode { get; set; }
+ public UInt16 DigitalOutput3Mode { get; set; }
+ public UInt16 DigitalOutput4Mode { get; set; }
+ public UInt16 DigitalOutput5Mode { get; set; }
+ public UInt16 PulseOut0LowTime { get; set; }
+ public UInt16 PulseOut1LowTime { get; set; }
+ public UInt16 PulseOut2LowTime { get; set; }
+ public UInt16 PulseOut3LowTime { get; set; }
+ public UInt16 PulseOut4LowTime { get; set; }
+ public UInt16 PulseOut5LowTime { get; set; }
+ public UInt16 PulseOut0HighTime { get; set; }
+ public UInt16 PulseOut1HighTime { get; set; }
+ public UInt16 PulseOut2HighTime { get; set; }
+ public UInt16 PulseOut3HighTime { get; set; }
+ public UInt16 PulseOut4HighTime { get; set; }
+ public UInt16 PulseOut5HighTime { get; set; }
+
+ public void PerformSolidGreenLed()
+ {
+ Console.WriteLine("Solid Green: This is not yet implemented ");
+ }
+
+ public void PerformSlowFlashingGreenLed()
+ {
+ Console.WriteLine("Slow Flashing Green: This is not yet implemented ");
+ }
+
+ public void PerformFastFlashingGreenLed()
+ {
+ Console.WriteLine("Fast Flashing Green: This is not yet implemented ");
+ }
+
+ public void PerformSolidOrangeLed()
+ {
+ Console.WriteLine("Solid Orange: This is not yet implemented ");
+ }
+
+ public void PerformSlowFlashingOrangeLed()
+ {
+ Console.WriteLine("Slow Flashing Orange: This is not yet implemented ");
+ }
+
+ public void PerformFastFlashingOrangeLed()
+ {
+ Console.WriteLine("Fast Flashing Orange: This is not yet implemented ");
+ }
+
+ public void PerformSolidRedLed()
+ {
+ Console.WriteLine("Solid Red: This is not yet implemented ");
+ }
+
+ public void PerformSlowFlashingRedLed()
+ {
+ Console.WriteLine("Slow Flashing Red: This is not yet implemented ");
+ }
+
+ public void PerformFastFlashingRedLed()
+ {
+ Console.WriteLine("Fast Flashing Red: This is not yet implemented ");
+ }
+
+ public IEnumerable K3InverterIsConnectedToIslandBus
+ {
+ get
+ {
+ yield return K3Inverter1IsConnectedToIslandBus;
+ yield return K3Inverter2IsConnectedToIslandBus;
+ yield return K3Inverter3IsConnectedToIslandBus;
+ yield return K3Inverter4IsConnectedToIslandBus;
+ }
+ }
+
+ private Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput16;
+ private Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput17;
+ private Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput18;
+ private Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput19;
+
+ public Boolean FiWarning => !_Regs.DigitalInput21;
+ public Boolean FiError => !_Regs.DigitalInput23;
+
+ public Boolean K2ConnectIslandBusToGridBus
+ {
+ get => _Regs.Relay23;
+ set => _Regs.Relay23 = value;
+ }
+
+ public static implicit operator Amax5070Registers(RelaysRecordAmax d) => d._Regs;
+ public static implicit operator RelaysRecordAmax(Amax5070Registers d) => new RelaysRecordAmax(d);
+
+}
diff --git a/csharp/App/SodiStoreMax/src/Switch.cs b/csharp/App/SodiStoreMax/src/Switch.cs
new file mode 100644
index 000000000..932fdb8df
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Switch.cs
@@ -0,0 +1,15 @@
+using InnovEnergy.Lib.Utils;
+
+namespace InnovEnergy.App.SodiStoreMax;
+
+public static class Switch
+{
+ public static TextBlock Open(String name)
+ {
+ return TextBlock.AlignCenterHorizontal
+ (
+ " __╱ __ ",
+ name
+ );
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/System/Controller.cs b/csharp/App/SodiStoreMax/src/System/Controller.cs
new file mode 100644
index 000000000..4285236d4
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/System/Controller.cs
@@ -0,0 +1,726 @@
+using InnovEnergy.App.SodiStoreMax.Ess;
+using InnovEnergy.App.SodiStoreMax.SaliMaxRelays;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
+using static InnovEnergy.Lib.Devices.Trumpf.SystemControl.DataTypes.GridType;
+
+namespace InnovEnergy.App.SodiStoreMax.System;
+
+public static class Controller
+{
+ private static Int32 GetSystemState(this StatusRecord r)
+ {
+ var relays = r.Relays;
+
+ if (relays is null)
+ return 101; // Message = "Panic: relay device is not available!",
+
+ var acDcs = r.AcDc;
+
+ if (acDcs.NotAvailable())
+ return 102;
+
+ var k4 = acDcs.AllGridTied() ? 0
+ : acDcs.AllIsland() ? 1
+ : 4;
+
+ var k5 = acDcs.AllDisabled() ? 0
+ : acDcs.AllEnabled() ? 1
+ : 4;
+
+ if (k4 == 4 || k5 == 4)
+ return 103; //Message = "Panic: ACDCs have unequal grid types or power stage",
+
+ var nInverters = r.AcDc.Devices.Count;
+
+ var k1 = relays.K1GridBusIsConnectedToGrid ? 1 : 0;
+ var k2 = relays.K2IslandBusIsConnectedToGridBus ? 1 : 0;
+ var k3 = relays.K3InverterIsConnectedToIslandBus.Take(nInverters).Any(c => c) ? 1 : 0;
+
+
+ // states as defined in states excel sheet
+ return 1 * k1
+ + 2 * k2
+ + 4 * k3
+ + 8 * k4
+ + 16 * k5;
+ }
+
+ public static Boolean ControlSystemState(this StatusRecord s)
+ {
+ s.StateMachine.State = s.GetSystemState();
+
+ return s.StateMachine.State switch
+ {
+ 0 => State0(s),
+ 1 => State1(s),
+ 2 => State2(s),
+ 3 => State3(s),
+ 4 => State4(s),
+ 5 => State5(s),
+ 6 => State6(s),
+ 7 => State7(s),
+ 8 => State8(s),
+ 9 => State9(s),
+ 10 => State10(s),
+ 11 => State11(s),
+ 12 => State12(s),
+ 13 => State13(s),
+ 14 => State14(s),
+ 15 => State15(s),
+ 16 => State16(s),
+ 17 => State17(s),
+ 18 => State18(s),
+ 19 => State19(s),
+ 20 => State20(s),
+ 21 => State21(s),
+ 22 => State22(s),
+ 23 => State23(s),
+ 24 => State24(s),
+ 25 => State25(s),
+ 26 => State26(s),
+ 27 => State27(s),
+ 28 => State28(s),
+ 29 => State29(s),
+ 30 => State30(s),
+ 31 => State31(s),
+
+
+ 101 => State101(s),
+ 102 => State102(s),
+ 103 => State103(s),
+ _ => UnknownState(s)
+ };
+
+ }
+
+ private static Boolean NotAvailable(this AcDcDevicesRecord acDcs)
+ {
+ return acDcs.SystemControl == null || acDcs.Devices.Count == 0;
+ }
+
+ private static Boolean State0(StatusRecord s)
+ {
+ s.StateMachine.Message = "Ac/Dc are off. Switching to Island Mode.";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 8
+ }
+
+
+ private static Boolean State1(StatusRecord s)
+ {
+ s.StateMachine.Message = "Grid Tied mode active, closing k2";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.ConnectIslandBusToGrid();
+
+ return false;
+
+ // => 3
+ }
+
+ private static Boolean State2(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 0
+ }
+
+ private static Boolean State3(StatusRecord s)
+ {
+ s.StateMachine.Message = "K2 closed, Turning on Ac/Dc";
+
+ s.DcDc.Enable();
+ s.AcDc.Enable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.ConnectIslandBusToGrid();
+
+ return false;
+
+ // => 19
+ }
+
+
+ private static Boolean State4(StatusRecord s)
+ {
+ s.StateMachine.Message = "K2 is open, waiting K3 to open";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return true;
+
+ // => 0
+ }
+
+ private static Boolean State5(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => ?
+ }
+
+ private static Boolean State6(StatusRecord s)
+ {
+ s.StateMachine.Message = "Inverters are off, opening K2";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return true;
+
+ // => 4
+ }
+
+ private static Boolean State7(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.ConnectIslandBusToGrid();
+
+ return false;
+
+ // => ?
+ }
+
+ private static Boolean State8(StatusRecord s)
+ {
+ s.StateMachine.Message = "Ac/Dc are off and in Island Mode.";
+
+ s.DcDc.Enable();
+ s.AcDc.Enable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return true;
+ // => 24
+ }
+
+ private static Boolean State9(StatusRecord s)
+ {
+ s.StateMachine.Message = "Ac/Dc are disconnected from Island Bus. Switching to GridTie Mode.";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 1
+ }
+
+ private static Boolean State10(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 8
+ }
+
+ private static Boolean State11(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 9
+ }
+
+ private static Boolean State12(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 8
+ }
+
+ private static Boolean State13(StatusRecord s)
+ {
+ s.StateMachine.Message = "Ac/Dc are off. Waiting for them to disconnect from Island Bus.";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return true;
+
+ // => 9
+ }
+
+ private static Boolean State14(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 12
+ }
+
+
+ private static Boolean State15(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 13
+ }
+
+
+ private static Boolean State16(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 0
+ }
+
+
+ private static Boolean State17(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 1
+ }
+
+
+ private static Boolean State18(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 0
+ }
+
+ private static Boolean State19(StatusRecord s)
+ {
+ s.StateMachine.Message = "Waiting for Ac/Dc to connect to Island Bus";
+
+ s.DcDc.Enable();
+ s.AcDc.Enable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.ConnectIslandBusToGrid();
+
+ return true;
+
+ // => 23
+ }
+
+
+ private static Boolean State20(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 4
+ }
+
+ private static Boolean State21(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 5
+ }
+
+ private static Boolean State22(StatusRecord s)
+ {
+ s.StateMachine.Message = "K1 opened, switching inverters off";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.ConnectIslandBusToGrid();
+
+ return true;
+
+ // => 6
+ }
+
+ private static Boolean State23(StatusRecord s)
+ {
+ s.StateMachine.Message = "ESS";
+
+ s.DcDc.Enable();
+ s.AcDc.Enable();
+ s.AcDc.EnableGridTieMode();
+ s.Relays.ConnectIslandBusToGrid();
+
+ return true;
+
+ // => 22
+ }
+
+
+ private static Boolean State24(StatusRecord s)
+ {
+ s.StateMachine.Message = "Inverter are on waiting for k3 to close";
+
+ s.DcDc.Enable();
+ s.AcDc.Enable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 28
+ }
+
+ private static Boolean State25(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 9
+ }
+
+ private static Boolean State26(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 10
+ }
+
+ private static Boolean State27(StatusRecord s)
+ {
+ s.StateMachine.Message = "K2 open and enable island mode";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 9
+ }
+
+ private static Boolean State28(StatusRecord s)
+ {
+ s.StateMachine.Message = "Island Mode";
+
+ s.DcDc.Enable();
+ s.AcDc.Enable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 29
+ }
+
+ private static Boolean State29(StatusRecord s)
+ {
+ s.StateMachine.Message = "K1 closed, Switching off Inverters and moving to grid tie";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 13
+ }
+
+ private static Boolean State30(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 14
+ }
+
+ private static Boolean State31(StatusRecord s)
+ {
+ s.StateMachine.Message = "";
+
+ s.DcDc.Disable();
+ s.AcDc.Disable();
+ s.AcDc.EnableIslandMode();
+ s.Relays.DisconnectIslandBusFromGrid();
+
+ return false;
+
+ // => 13
+ }
+
+ private static Boolean State101(StatusRecord s)
+ {
+ s.StateMachine.Message = "Relay device is not available";
+ return s.EnableSafeDefaults();
+ }
+
+ private static Boolean State102(StatusRecord s)
+ {
+ s.StateMachine.Message = "ACDCs not available";
+ return s.EnableSafeDefaults();
+ }
+
+ private static Boolean State103(StatusRecord s)
+ {
+ s.StateMachine.Message = "Panic: ACDCs have unequal grid types or PowerStage";
+ return s.EnableSafeDefaults();
+ }
+
+ // private static Boolean State104(StatusRecord s)
+ // {
+ // s.StateMachine.Message = "Panic: DCDCs not available";
+ // return s.EnableSafeDefaults();
+ // }
+
+
+ private static Boolean UnknownState(StatusRecord s)
+ {
+ // "Unknown System State"
+ return s.EnableSafeDefaults();
+ }
+
+
+
+ private static Boolean AllDisabled(this AcDcDevicesRecord acDcs)
+ {
+ return acDcs.Devices.All(d => !d.Control.PowerStageEnable);
+ }
+
+ private static Boolean AllEnabled(this AcDcDevicesRecord acDcs)
+ {
+ return acDcs.Devices.All(d => d.Control.PowerStageEnable);
+ }
+
+
+ private static Boolean AllTheSame(this AcDcDevicesRecord acDcs)
+ {
+ return acDcs.Devices
+ .Select(d => d.Control.PowerStageEnable)
+ .Distinct()
+ .Count() == 1;
+ }
+
+ private static Boolean AllGridTied(this AcDcDevicesRecord acDcs)
+ {
+ return acDcs.Devices.All(d => d.Status.ActiveGridType is GridTied380V60Hz)
+ || acDcs.Devices.All(d => d.Status.ActiveGridType is GridTied400V50Hz)
+ || acDcs.Devices.All(d => d.Status.ActiveGridType is GridTied480V60Hz);
+ }
+
+ private static Boolean AllIsland(this AcDcDevicesRecord acDcs)
+ {
+ return acDcs.Devices.All(d => d.Status.ActiveGridType is Island400V50Hz)
+ || acDcs.Devices.All(d => d.Status.ActiveGridType is Island480V60Hz);
+ }
+
+ private static void ForAll(this IEnumerable ts, Action action)
+ {
+ foreach (var t in ts)
+ action(t);
+ }
+
+ private static void Disable(this AcDcDevicesRecord acDc)
+ {
+ acDc.Devices
+ .Select(d => d.Control)
+ .ForAll(c => c.PowerStageEnable = false);
+ }
+
+ //this is must be deleted
+ private static void Disable(this DcDcDevicesRecord dcDc)
+ {
+ // For Test purpose, The transition from island mode to grid tier and vis versa , may not need to disable Dc/Dc.
+ // This will keep the Dc link powered.
+
+ // dcDc.Devices
+ // .Select(d => d.Control)
+ // .ForAll(c => c.PowerStageEnable = false);
+ }
+
+ private static void Enable(this AcDcDevicesRecord acDc)
+ {
+ acDc.Devices
+ .Select(d => d.Control)
+ .ForAll(c => c.PowerStageEnable = true);
+ }
+
+ private static void Enable(this DcDcDevicesRecord dcDc)
+ {
+ dcDc.Devices
+ .Select(d => d.Control)
+ .ForAll(c => c.PowerStageEnable = true);
+ }
+
+
+ private static void EnableGridTieMode(this AcDcDevicesRecord acDc)
+ {
+ acDc.Devices
+ .Select(d => d.Control)
+ .ForAll(c => c.Ac.GridType = GridTied400V50Hz); // TODO: config grid type
+ }
+
+
+ private static void EnableIslandMode(this AcDcDevicesRecord acDc)
+ {
+ acDc.Devices
+ .Select(d => d.Control)
+ .ForAll(c => c.Ac.GridType = Island400V50Hz); // TODO: config grid type
+ }
+
+ private static void DisconnectIslandBusFromGrid(this IRelaysRecord? relays)
+ {
+ if (relays is not null)
+ relays.K2ConnectIslandBusToGridBus = false;
+ }
+
+ private static void ConnectIslandBusToGrid(this IRelaysRecord? relays)
+ {
+ if (relays is not null)
+ relays.K2ConnectIslandBusToGridBus = true;
+ }
+
+
+ private static Boolean EnableSafeDefaults(this StatusRecord s)
+ {
+ // After some tests, the safe state is switch off inverter and keep the last state of K2 , Dc/Dc and Grid type to avoid conflict.
+
+ // s.DcDc.Disable();
+ s.AcDc.Disable(); // Maybe comment this to avoid opening/closing K3
+ // s.AcDc.EnableGridTieMode();
+ // s.Relays.DisconnectIslandBusFromGrid();
+ return false;
+ }
+
+ public static DcDcDevicesRecord ResetAlarms(this DcDcDevicesRecord dcDcStatus)
+ {
+ var sc = dcDcStatus.SystemControl;
+
+ if (sc is not null)
+ sc.ResetAlarmsAndWarnings = sc.Alarms.Any();
+
+ foreach (var d in dcDcStatus.Devices)
+ d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
+
+ return dcDcStatus;
+ }
+
+ public static AcDcDevicesRecord ResetAlarms(this AcDcDevicesRecord acDcStatus)
+ {
+ var sc = acDcStatus.SystemControl;
+
+ if (sc is not null)
+ sc.ResetAlarmsAndWarnings = sc.Alarms.Any() || sc.Warnings.Any();
+
+ foreach (var d in acDcStatus.Devices)
+ d.Control.ResetAlarmsAndWarnings = d.Status.Alarms.Any() || d.Status.Warnings.Any();
+
+ return acDcStatus;
+ }
+
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/System/StateMachine.cs b/csharp/App/SodiStoreMax/src/System/StateMachine.cs
new file mode 100644
index 000000000..6e0d498ec
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/System/StateMachine.cs
@@ -0,0 +1,9 @@
+namespace InnovEnergy.App.SodiStoreMax.System;
+
+public record StateMachine
+{
+ public required String Message { get; set; } // TODO: init only
+ public required Int32 State { get; set; } // TODO: init only
+
+ public static StateMachine Default { get; } = new StateMachine { State = 100, Message = "Unknown State" };
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SystemConfig/AcDcConfig.cs b/csharp/App/SodiStoreMax/src/SystemConfig/AcDcConfig.cs
new file mode 100644
index 000000000..28b514dc4
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SystemConfig/AcDcConfig.cs
@@ -0,0 +1,8 @@
+namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
+
+public class AcDcConfig
+{
+ public required Double MaxDcLinkVoltage { get; set; }
+ public required Double MinDcLinkVoltage { get; set; }
+ public required Double ReferenceDcLinkVoltage { get; init; }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SystemConfig/CalibrationChargeType.cs b/csharp/App/SodiStoreMax/src/SystemConfig/CalibrationChargeType.cs
new file mode 100644
index 000000000..1b783b808
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SystemConfig/CalibrationChargeType.cs
@@ -0,0 +1,8 @@
+namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
+
+public enum CalibrationChargeType
+{
+ RepetitivelyEvery,
+ AdditionallyOnce,
+ ChargePermanently
+}
diff --git a/csharp/App/SodiStoreMax/src/SystemConfig/Config.cs b/csharp/App/SodiStoreMax/src/SystemConfig/Config.cs
new file mode 100644
index 000000000..110db56cb
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SystemConfig/Config.cs
@@ -0,0 +1,272 @@
+using System.Text.Json;
+using InnovEnergy.App.SodiStoreMax.Devices;
+using InnovEnergy.Lib.Utils;
+using static System.Text.Json.JsonSerializer;
+
+namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
+
+// shut up trim warnings
+#pragma warning disable IL2026
+
+public class Config //TODO: let IE choose from config files (Json) and connect to GUI
+{
+ private static String DefaultConfigFilePath => Path.Combine(Environment.CurrentDirectory, "config.json");
+ private static DateTime DefaultDatetime => new(2024, 03, 11, 09, 00, 00);
+
+ private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
+
+ public required Double MinSoc { get; set; }
+ public required UInt16 CurtailP { get; set; }// in Kw
+ public required UInt16 PvInstalledPower { get; set; }// in Kw
+ public required CalibrationChargeType ForceCalibrationChargeState { get; set; }
+ public required DateTime DayAndTimeForRepetitiveCalibration { get; set; }
+ public required DateTime DayAndTimeForAdditionalCalibration { get; set; }
+ public required Boolean DisplayIndividualBatteries { get; set; }
+ public required Double PConstant { get; set; }
+ public required Double GridSetPoint { get; set; }
+ public required Double BatterySelfDischargePower { get; set; }
+ public required Double HoldSocZone { get; set; }
+ public required DevicesConfig IslandMode { get; set; }
+ public required DevicesConfig GridTie { get; set; }
+
+ public required DeviceConfig Devices { get; set; }
+ public required S3Config? S3 { get; set; }
+
+ private static String? LastSavedData { get; set; }
+
+ #if DEBUG
+ public static Config Default => new()
+ {
+ MinSoc = 20,
+ CurtailP = 0,
+ PvInstalledPower = 20,
+ ForceCalibrationChargeState = CalibrationChargeType.RepetitivelyEvery,
+ DayAndTimeForRepetitiveCalibration = DefaultDatetime,
+ DayAndTimeForAdditionalCalibration = DefaultDatetime,
+ DisplayIndividualBatteries = false,
+ PConstant = .5,
+ GridSetPoint = 0,
+ BatterySelfDischargePower = 200,
+ HoldSocZone = 1, // TODO: find better name,
+ IslandMode = new()
+ {
+ AcDc = new ()
+ {
+ MinDcLinkVoltage = 690,
+ ReferenceDcLinkVoltage = 750,
+ MaxDcLinkVoltage = 810,
+ },
+
+ DcDc = new ()
+ {
+ UpperDcLinkVoltage = 50,
+ LowerDcLinkVoltage = 50,
+ ReferenceDcLinkVoltage = 750,
+
+ MaxBatteryChargingCurrent = 210,
+ MaxBatteryDischargingCurrent = 210,
+ MaxDcPower = 10000,
+
+ MaxChargeBatteryVoltage = 57,
+ MinDischargeBatteryVoltage = 0,
+ },
+ },
+
+ GridTie = new()
+ {
+ AcDc = new ()
+ {
+ MinDcLinkVoltage = 720,
+ ReferenceDcLinkVoltage = 750,
+ MaxDcLinkVoltage = 810,
+ },
+
+ DcDc = new ()
+ {
+ UpperDcLinkVoltage = 50,
+ LowerDcLinkVoltage = 50,
+ ReferenceDcLinkVoltage = 750,
+
+ MaxBatteryChargingCurrent = 210,
+ MaxBatteryDischargingCurrent = 210,
+ MaxDcPower = 10000,
+
+ MaxChargeBatteryVoltage = 57,
+ MinDischargeBatteryVoltage = 0,
+ },
+ },
+
+ Devices = new ()
+ {
+ RelaysIp = new() { Host = "localhost", Port = 5006, DeviceState = DeviceState.Measured},
+ TsRelaysIp = new() { Host = "localhost", Port = 5006, DeviceState = DeviceState.Measured},
+ GridMeterIp = new() { Host = "localhost", Port = 5003, DeviceState = DeviceState.Measured},
+ PvOnAcGrid = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
+ LoadOnAcGrid = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
+ PvOnAcIsland = new() { Host = "true" , Port = 0 , DeviceState = DeviceState.Measured},
+ IslandBusLoadMeterIp = new() { Host = "localhost", Port = 5004, DeviceState = DeviceState.Measured},
+ TruConvertAcIp = new() { Host = "localhost", Port = 5001, DeviceState = DeviceState.Measured},
+ PvOnDc = new() { Host = "localhost", Port = 5005, DeviceState = DeviceState.Measured},
+ LoadOnDc = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
+ TruConvertDcIp = new() { Host = "localhost", Port = 5002, DeviceState = DeviceState.Measured},
+ BatteryIp = new() { Host = "localhost", Port = 5007, DeviceState = DeviceState.Measured},
+ BatteryNodes = new []{ 2, 3, 4, 5, 6 }
+ },
+
+ S3 = new()
+ {
+ Bucket = "1-3e5b3069-214a-43ee-8d85-57d72000c19d",
+ Region = "sos-ch-dk-2",
+ Provider = "exo.io",
+ ContentType = "text/plain; charset=utf-8",
+ Key = "EXO4ec5faf1a7650b79b5722fb5",
+ Secret = "LUxu1PGEA-POEIckoEyq6bYyz0RnenW6tmqccMKgkHQ"
+ },
+ };
+ #else
+ public static Config Default => new()
+ {
+ MinSoc = 20,
+ CurtailP = 0,
+ PvInstalledPower = 20,
+ ForceCalibrationChargeState = CalibrationChargeType.RepetitivelyEvery,
+ DayAndTimeForRepetitiveCalibration = DefaultDatetime,
+ DayAndTimeForAdditionalCalibration = DefaultDatetime,
+ DisplayIndividualBatteries = false,
+ PConstant = .5,
+ GridSetPoint = 0,
+ BatterySelfDischargePower = 200,
+ HoldSocZone = 1, // TODO: find better name,
+ IslandMode = new()
+ {
+ AcDc = new ()
+ {
+ MinDcLinkVoltage = 690,
+ ReferenceDcLinkVoltage = 750,
+ MaxDcLinkVoltage = 810,
+ },
+
+ DcDc = new ()
+ {
+ UpperDcLinkVoltage = 50,
+ LowerDcLinkVoltage = 50,
+ ReferenceDcLinkVoltage = 750,
+
+ MaxBatteryChargingCurrent = 210,
+ MaxBatteryDischargingCurrent = 210,
+ MaxDcPower = 10000,
+
+ MaxChargeBatteryVoltage = 57,
+ MinDischargeBatteryVoltage = 0,
+ },
+ },
+
+ GridTie = new()
+ {
+ AcDc = new ()
+ {
+ MinDcLinkVoltage = 720,
+ ReferenceDcLinkVoltage = 750,
+ MaxDcLinkVoltage = 780,
+ },
+
+ DcDc = new ()
+ {
+ UpperDcLinkVoltage = 20,
+ LowerDcLinkVoltage = 20,
+ ReferenceDcLinkVoltage = 750,
+
+ MaxBatteryChargingCurrent = 210,
+ MaxBatteryDischargingCurrent = 210,
+ MaxDcPower = 10000,
+
+ MaxChargeBatteryVoltage = 57,
+ MinDischargeBatteryVoltage = 0,
+ },
+ },
+
+
+ S3 = new()
+ {
+ Bucket = "1-3e5b3069-214a-43ee-8d85-57d72000c19d",
+ Region = "sos-ch-dk-2",
+ Provider = "exo.io",
+ Key = "EXObb5a49acb1061781761895e7",
+ Secret = "sKhln0w8ii3ezZ1SJFF33yeDo8NWR1V4w2H0D4-350I",
+ ContentType = "text/plain; charset=utf-8"
+ },
+
+ Devices = new ()
+ {
+ RelaysIp = new() { Host = "10.0.1.1", Port = 502, DeviceState = DeviceState.Measured},
+ TsRelaysIp = new() { Host = "10.0.1.2", Port = 502, DeviceState = DeviceState.Measured},
+ GridMeterIp = new() { Host = "10.0.4.1", Port = 502, DeviceState = DeviceState.Measured},
+ PvOnAcGrid = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
+ LoadOnAcGrid = new() { Host = "true" , Port = 0 , DeviceState = DeviceState.Measured},
+ PvOnAcIsland = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
+ IslandBusLoadMeterIp = new() { Host = "10.0.4.2", Port = 502, DeviceState = DeviceState.Measured},
+ TruConvertAcIp = new() { Host = "10.0.2.1", Port = 502, DeviceState = DeviceState.Measured},
+ PvOnDc = new() { Host = "10.0.5.1", Port = 502, DeviceState = DeviceState.Measured},
+ LoadOnDc = new() { Host = "false" , Port = 0 , DeviceState = DeviceState.Measured},
+ TruConvertDcIp = new() { Host = "10.0.3.1", Port = 502, DeviceState = DeviceState.Measured},
+ BatteryIp = new() { Host = "localhost", Port = 6855, DeviceState = DeviceState.Measured },
+ BatteryNodes = new []{ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
+ },
+ };
+ #endif
+
+ public void Save(String? path = null)
+ {
+ var configFilePath = path ?? DefaultConfigFilePath;
+
+ try
+ {
+ var jsonString = Serialize(this, JsonOptions);
+
+ if (LastSavedData == jsonString)
+ return;
+
+ LastSavedData = jsonString;
+
+ File.WriteAllText(configFilePath, jsonString);
+ }
+ catch (Exception e)
+ {
+ $"Failed to write config file {configFilePath}\n{e}".WriteLine();
+ throw;
+ }
+ }
+
+
+ public static Config Load(String? path = null)
+ {
+ var configFilePath = path ?? DefaultConfigFilePath;
+ try
+ {
+ var jsonString = File.ReadAllText(configFilePath);
+ return Deserialize(jsonString)!;
+ }
+ catch (Exception e)
+ {
+ $"Failed to read config file {configFilePath}, using default config\n{e}".WriteLine();
+ return Default;
+ }
+ }
+
+
+ public static async Task LoadAsync(String? path = null)
+ {
+ var configFilePath = path ?? DefaultConfigFilePath;
+ try
+ {
+ var jsonString = await File.ReadAllTextAsync(configFilePath);
+ return Deserialize(jsonString)!;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Couldn't read config file {configFilePath}, using default config");
+ e.Message.WriteLine();
+ return Default;
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SystemConfig/DcDcConfig.cs b/csharp/App/SodiStoreMax/src/SystemConfig/DcDcConfig.cs
new file mode 100644
index 000000000..182f71521
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SystemConfig/DcDcConfig.cs
@@ -0,0 +1,15 @@
+namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
+
+public class DcDcConfig
+{
+ public required Double LowerDcLinkVoltage { get; set; }
+ public required Double ReferenceDcLinkVoltage { get; init; }
+ public required Double UpperDcLinkVoltage { get; set; }
+
+ public required Double MaxBatteryChargingCurrent { get; set; }
+ public required Double MaxBatteryDischargingCurrent { get; set; }
+ public required Double MaxDcPower { get; set; }
+
+ public required Double MaxChargeBatteryVoltage { get; set; }
+ public required Double MinDischargeBatteryVoltage { get; set; }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/SystemConfig/DeviceConfig.cs b/csharp/App/SodiStoreMax/src/SystemConfig/DeviceConfig.cs
new file mode 100644
index 000000000..aed893168
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SystemConfig/DeviceConfig.cs
@@ -0,0 +1,21 @@
+using InnovEnergy.App.SodiStoreMax.Devices;
+using InnovEnergy.Lib.Utils.Net;
+
+namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
+
+public class DeviceConfig
+{
+ public required SalimaxDevice RelaysIp { get; init; }
+ public required SalimaxDevice TsRelaysIp { get; init; }
+ public required SalimaxDevice GridMeterIp { get; init; }
+ public required SalimaxDevice PvOnAcGrid { get; init; }
+ public required SalimaxDevice LoadOnAcGrid { get; init; }
+ public required SalimaxDevice PvOnAcIsland { get; init; }
+ public required SalimaxDevice IslandBusLoadMeterIp { get; init; }
+ public required SalimaxDevice TruConvertAcIp { get; init; }
+ public required SalimaxDevice PvOnDc { get; init; }
+ public required SalimaxDevice LoadOnDc { get; init; }
+ public required SalimaxDevice TruConvertDcIp { get; init; }
+ public required SalimaxDevice BatteryIp { get; init; }
+ public required Int32[] BatteryNodes { get; init; }
+}
diff --git a/csharp/App/SodiStoreMax/src/SystemConfig/DevicesConfig.cs b/csharp/App/SodiStoreMax/src/SystemConfig/DevicesConfig.cs
new file mode 100644
index 000000000..ba049f5cb
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/SystemConfig/DevicesConfig.cs
@@ -0,0 +1,7 @@
+namespace InnovEnergy.App.SodiStoreMax.SystemConfig;
+
+public class DevicesConfig
+{
+ public required AcDcConfig AcDc { get; init; }
+ public required DcDcConfig DcDc { get; init; }
+}
\ No newline at end of file
diff --git a/csharp/App/SodiStoreMax/src/Topology.cs b/csharp/App/SodiStoreMax/src/Topology.cs
new file mode 100644
index 000000000..5ad0190e1
--- /dev/null
+++ b/csharp/App/SodiStoreMax/src/Topology.cs
@@ -0,0 +1,530 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using InnovEnergy.App.SodiStoreMax.Devices;
+using InnovEnergy.App.SodiStoreMax.Ess;
+using InnovEnergy.Lib.Devices.AMPT;
+using InnovEnergy.Lib.Devices.BatteryDeligreen;
+using InnovEnergy.Lib.Devices.EmuMeter;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertAc;
+using InnovEnergy.Lib.Devices.Trumpf.TruConvertDc;
+using InnovEnergy.Lib.Units;
+using InnovEnergy.Lib.Units.Power;
+using InnovEnergy.Lib.Utils;
+using Ac3Bus = InnovEnergy.Lib.Units.Composite.Ac3Bus;
+
+namespace InnovEnergy.App.SodiStoreMax;
+
+// ┌────┐ ┌────┐
+// │ Pv │ │ Pv │ ┌────┐
+// └────┘ └────┘ │ Pv │
+// V V └────┘
+// V V V
+// (b) 0 W (e) 0 W V
+// V V (i) 13.2 kW ┌────────────┐
+// V V V │ Battery │
+// ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌─────────┐ V ├────────────┤
+// │ Grid │ │ Grid Bus │ │ Island Bus │ │ AC/DC │ ┌────────┐ ┌───────┐ │ 52.3 V │
+// ├─────────┤ -10.3 kW ├──────────┤ -11.7 kW ├────────────┤ -11.7 kW ├─────────┤ -11.7 kW │ Dc Bus │ 1008 W │ DC/DC │ 1008 W │ 99.1 % │
+// │ -3205 W │<<<<<<<<<<│ 244 V │<<<<<<<<<<│ 244 V │<<<<<<<<<<│ -6646 W │<<<<<<<<<<├────────┤>>>>>>>>>>├───────┤>>>>>>>>>>│ 490 mA │
+// │ -3507 W │ (a) │ 244 V │ (d) │ 244 V │ (g) │ -5071 W │ (h) │ 776 V │ (k) │ 56 V │ (l) │ 250 °C │
+// │ -3605 W │ K1 │ 246 V │ K2 │ 246 V │ K3 └─────────┘ └────────┘ └───────┘ │ 445 A │
+// └─────────┘ └──────────┘ └────────────┘ V │ 0 Warnings │
+// V V V │ 0 Alarms │
+// V V (j) 0 W └────────────┘
+// (c) 1400 W (f) 0 W V
+// V V V
+// V V ┌──────┐
+// ┌──────┐ ┌──────┐ │ Load │
+// │ Load │ │ Load │ └──────┘
+// └──────┘ └──────┘
+
+
+// Calculated values: c,d & h
+// ==========================
+//
+//
+// AC side
+// a + b - c - d = 0 [eq1]
+// d + e - f - g = 0 [eq2]
+//
+// c & d are not measured!
+//
+// d = f + g - e [eq2]
+// c = a + b - d [eq1]
+//
+// DC side
+// h + i - j - k = 0 [eq3]
+//
+// if Dc load not existing, h = i - k [eq4]
+
+// k = l assuming no losses in DCDC // this is changed now l is equal total battery power
+// j = h + i - k [eq3]
+
+
+public static class Topology
+{
+
+ public static TextBlock CreateTopologyTextBlock(this StatusRecord status)
+ {
+ var a = status.GridMeter?.Ac.Power.Active;
+ var b = status.PvOnAcGrid?.Dc.Power.Value;
+ var e = status.PvOnAcIsland?.Dc.Power.Value;
+ var f = status.LoadOnAcIsland?.Ac.Power.Active;
+ var g = status.AcDc.Dc.Power.Value;
+ var h = status.AcDcToDcLink?.Power.Value;
+ var i = status.PvOnDc?.Dc.Power.Value;
+ var k = status.DcDc.Dc.Link.Power.Value;
+ var l = status.Battery is not null ? status.Battery.Power : 0;
+ var j = status.LoadOnDc?.Power.Value;
+ var d = status.AcGridToAcIsland?.Power.Active;
+ var c = status.LoadOnAcGrid?.Power.Active;
+
+ /////////////////////////////
+
+ var grid = status.CreateGridColumn(a);
+ var gridBus = status.CreateGridBusColumn(b, c, d);
+ var islandBus = status.CreateIslandBusColumn(e, f, g);
+ var inverter = status.CreateInverterColumn(h);
+ var dcBus = status.CreateDcBusColumn(i, j, k);
+ var dcDc = status.CreateDcDcColumn(l);
+ var batteries = status.CreateBatteryColumn();
+
+ return TextBlock.AlignCenterVertical
+ (
+ grid,
+ gridBus,
+ islandBus,
+ inverter,
+ dcBus,
+ dcDc,
+ batteries
+ );
+ }
+
+ private static TextBlock CreateGridColumn(this StatusRecord status, ActivePower? a)
+ {
+ // ┌─────────┐
+ // │ Grid │
+ // ├─────────┤ -10.3 kW
+ // │ -3205 W │<<<<<<<<<<
+ // │ -3507 W │ (a)
+ // │ -3605 W │ K1
+ // └─────────┘
+
+ var gridMeterAc = status.GridMeter?.Ac;
+ var k1 = status.Relays?.K1GridBusIsConnectedToGrid;
+
+ var gridBox = PhasePowersActive(gridMeterAc).TitleBox("Grid");
+ var gridFlow = SwitchedFlow(k1, a, "K1");
+
+ return TextBlock.AlignCenterVertical(gridBox, gridFlow);
+ }
+
+
+ private static TextBlock CreateGridBusColumn(this StatusRecord status,
+ ActivePower? b,
+ ActivePower? c,
+ ActivePower? d)
+ {
+
+ // ┌────┐
+ // │ Pv │
+ // └────┘
+ // V
+ // V
+ // (b) 0 W
+ // V
+ // V
+ // ┌──────────┐
+ // │ Grid Bus │
+ // ├──────────┤ -11.7 kW
+ // │ 244 V │<<<<<<<<<<
+ // │ 244 V │ (d)
+ // │ 246 V │ K2
+ // └──────────┘
+ // V
+ // V
+ // (c) 1400 W
+ // V
+ // V
+ // ┌──────┐
+ // │ Load │
+ // └──────┘
+
+
+ ////////////// top //////////////
+
+ var pvBox = TextBlock.FromString("PV").Box();
+ var pvFlow = Flow.Vertical(b);
+
+ ////////////// center //////////////
+
+ // on IslandBus show voltages measured by inverter
+ // on GridBus show voltages measured by grid meter
+ // ought to be approx the same
+
+ var gridMeterAc = status.GridMeter?.Ac;
+ var k2 = status.Relays?.K2IslandBusIsConnectedToGridBus;
+
+ var busBox = PhaseVoltages(gridMeterAc).TitleBox("Grid Bus");
+ var busFlow = SwitchedFlow(k2, d, "K2");
+
+ ////////////// bottom //////////////
+
+ var loadFlow = Flow.Vertical(c);
+ var loadBox = TextBlock.FromString("Load").Box();
+
+ ////////////// assemble //////////////
+
+ return TextBlock.AlignCenterVertical
+ (
+ TextBlock.AlignCenterHorizontal(pvBox, pvFlow, busBox, loadFlow, loadBox),
+ busFlow
+ );
+ }
+
+
+ private static TextBlock CreateIslandBusColumn(this StatusRecord status,
+ ActivePower? e,
+ ActivePower? f,
+ ActivePower? g)
+ {
+
+ // ┌────┐
+ // │ Pv │
+ // └────┘
+ // V
+ // V
+ // (e) 0 W
+ // V
+ // V
+ // ┌────────────┐
+ // │ Island Bus │
+ // ├────────────┤ -11.7 kW
+ // │ 244 V │<<<<<<<<<<
+ // │ 244 V │ (g)
+ // │ 246 V │ K3
+ // └────────────┘
+ // V
+ // V
+ // (f) 0 W
+ // V
+ // V
+ // ┌──────┐
+ // │ Load │
+ // └──────┘
+
+
+ ////////////// top //////////////
+
+ var pvBox = TextBlock.FromString("PV").Box();
+ var pvFlow = Flow.Vertical(e);
+
+ ////////////// center //////////////
+
+ // on IslandBus show voltages measured by inverter
+ // on GridBus show voltages measured by grid meter
+ // ought to be approx the same
+
+ var inverterAc = status.AcDc.Ac;
+ var busBox = PhaseVoltages(inverterAc).TitleBox("Island Bus");
+ var busFlow = status.IslandBusToInverterConnection(g);
+
+ ////////////// bottom //////////////
+
+ var loadFlow = Flow.Vertical(f);
+ var loadBox = TextBlock.FromString("Load").Box();
+
+ ////////////// assemble //////////////
+
+ return TextBlock.AlignCenterVertical
+ (
+ TextBlock.AlignCenterHorizontal(pvBox, pvFlow, busBox, loadFlow, loadBox),
+ busFlow
+ );
+ }
+
+
+
+ private static TextBlock CreateInverterColumn(this StatusRecord status, ActivePower? h)
+ {
+ // ┌─────────┐
+ // │ AC/DC │
+ // ├─────────┤ -11.7 kW
+ // │ -6646 W │<<<<<<<<<<
+ // │ -5071 W │ (h)
+ // └─────────┘
+
+ var inverterBox = status
+ .AcDc
+ .Devices
+ .Select(d => d.Status.Ac.Power.Active)
+ .Apply(TextBlock.AlignLeft)
+ .TitleBox("AC/DC");
+
+ var dcFlow = Flow.Horizontal(h);
+
+ return TextBlock.AlignCenterVertical(inverterBox, dcFlow);
+ }
+
+
+ [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
+ private static TextBlock IslandBusToInverterConnection(this StatusRecord status, ActivePower? g)
+ {
+ if (status.Relays is null)
+ return TextBlock.FromString("????????");
+
+ var nInverters = status.AcDc.Devices.Count;
+
+ var k3S = status
+ .Relays
+ .K3InverterIsConnectedToIslandBus
+ .Take(nInverters);
+
+ if (k3S.Prepend(true).All(s => s)) // TODO: display when no ACDC present
+ return Flow.Horizontal(g);
+
+ return Switch.Open("K3");
+ }
+
+ private static TextBlock CreateDcDcColumn(this StatusRecord status, ActivePower? p)
+ {
+ var dc48Voltage = status.DcDc.Dc.Battery.Voltage.ToDisplayString();
+
+ var busBox = TextBlock
+ .AlignLeft(dc48Voltage)
+ .TitleBox("DC/DC");
+
+ var busFlow = Flow.Horizontal(p);
+
+ return TextBlock.AlignCenterVertical(busBox, busFlow);
+ }
+
+ private static TextBlock CreateDcBusColumn(this StatusRecord status,
+ ActivePower? i,
+ ActivePower? j,
+ ActivePower? k)
+ {
+ // ┌────┐
+ // │ Pv │
+ // └────┘
+ // V
+ // V
+ // (i) 13.2 kW
+ // V
+ // V
+ // ┌────────┐
+ // │ Dc Bus │ 1008 W
+ // ├────────┤>>>>>>>>>>
+ // │ 776 V │ (k)
+ // └────────┘
+ // V
+ // V
+ // (j) 0 W
+ // V
+ // V
+ // ┌──────┐
+ // │ Load │
+ // └──────┘
+ //
+
+
+ /////////////////// top ///////////////////
+
+ var mppt = status.PvOnDc;
+
+ var nStrings = mppt is not null
+ ? "x" + mppt.Strings.Count
+ : "?";
+
+ var pvBox = TextBlock.FromString($"PV {nStrings}").Box();
+ var pvToBus = Flow.Vertical(i);
+
+ /////////////////// center ///////////////////
+
+ var dcBusVoltage = status.DcDc.Dc.Link.Voltage;
+
+ var dcBusBox = dcBusVoltage
+ .ToDisplayString()
+ .Apply(TextBlock.FromString)
+ .TitleBox("DC Bus ");
+
+ var busFlow = Flow.Horizontal(k);
+
+ /////////////////// bottom ///////////////////
+
+ var busToLoad = Flow.Vertical(j);
+ var loadBox = TextBlock.FromString("DC Load").Box();
+
+ ////////////// assemble //////////////
+
+ return TextBlock.AlignCenterVertical
+ (
+ TextBlock.AlignCenterHorizontal(pvBox, pvToBus, dcBusBox, busToLoad, loadBox),
+ busFlow
+ );
+ }
+
+ private static TextBlock CreateBatteryColumn(this StatusRecord status)
+ {
+ var bat = status.Battery;
+ if (bat is null)
+ return TextBlock.AlignLeft("no battery").Box();
+
+
+ var batteryAvgBox = CreateAveragedBatteryBox(bat);
+
+ var batteryBoxes = bat
+ .Devices
+ .Select(CreateBatteryBox)
+ .ToReadOnlyList();
+
+
+ var individualWithAvgBox = TextBlock
+ .AlignCenterVertical
+ (
+ batteryAvgBox ,
+ batteryBoxes.Any()
+ ? TextBlock.AlignLeft(batteryBoxes)
+ : TextBlock.Empty
+ );
+
+ return status.Config.DisplayIndividualBatteries ? individualWithAvgBox : batteryAvgBox;
+ }
+
+ private static TextBlock CreateAveragedBatteryBox(BatteryDeligreenRecords bat)
+ {
+ var voltage = bat.Voltage.ToDisplayString();
+ var soc = bat.Devices.Any() ? bat.Devices.Average(b => b.BatteryDeligreenDataRecord.Soc).Percent().ToDisplayString() : "0"; // TODO
+ var current = bat.Current.ToDisplayString();
+ var busCurrent = bat.Devices.Any() ? bat.Devices.Sum(b => b.BatteryDeligreenDataRecord.BusCurrent).A().ToDisplayString() : "0";
+ var temp = bat.TemperatureCell1.ToDisplayString();
+ //var alarms = bat.Alarms.Count + " Alarms";
+ //var warnings = bat.Warnings.Count + " Warnings";
+ var nBatteries = bat.Devices.Count;
+
+ return TextBlock
+ .AlignLeft
+ (
+ voltage,
+ soc,
+ current,
+ busCurrent,
+ temp
+ )
+ .TitleBox($"Battery x{nBatteries}");
+ }
+
+ private static TextBlock PhaseVoltages(Ac3Bus? ac)
+ {
+ return TextBlock.AlignLeft
+ (
+ ac?.L1.Voltage.ToDisplayString() ?? "???",
+ ac?.L2.Voltage.ToDisplayString() ?? "???",
+ ac?.L3.Voltage.ToDisplayString() ?? "???"
+ );
+ }
+
+ private static TextBlock PhasePowersActive(Ac3Bus? ac)
+ {
+ return TextBlock.AlignLeft
+ (
+ ac?.L1.Power.Active.ToDisplayString() ?? "???",
+ ac?.L2.Power.Active.ToDisplayString() ?? "???",
+ ac?.L3.Power.Active.ToDisplayString() ?? "???"
+ );
+ }
+
+ private static TextBlock CreateBatteryBox(BatteryDeligreenRecord battery, Int32 i)
+ {
+ var batteryWarnings = "";// battery.Warnings.Any();
+ var batteryAlarms = "";// battery.Alarms.Any();
+
+ var content = TextBlock.AlignLeft
+ (
+ battery.BatteryDeligreenDataRecord.BusVoltage.ToDisplayString(),
+ battery.BatteryDeligreenDataRecord.Soc.ToDisplayString(),
+ battery.BatteryDeligreenDataRecord.BusCurrent.ToDisplayString() + " C/D",
+ battery.BatteryDeligreenDataRecord.TemperaturesList.PowerTemperature.ToDisplayString(),
+ battery.BatteryDeligreenDataRecord.BatteryCapacity.ToString(CultureInfo.CurrentCulture) ,
+ batteryWarnings,
+ batteryAlarms
+ );
+
+ var box = content.TitleBox($"Battery {i + 1}");
+ var flow = Flow.Horizontal(battery.BatteryDeligreenDataRecord.Power);
+
+ return TextBlock.AlignCenterVertical(flow, box);
+ }
+
+
+ private static TextBlock SwitchedFlow(Boolean? switchClosed, ActivePower? power, String kx)
+ {
+ return switchClosed is null ? TextBlock.FromString("??????????")
+ : !switchClosed.Value ? Switch.Open(kx)
+ : power is null ? TextBlock.FromString("??????????")
+ : Flow.Horizontal(power);
+ }
+
+ //We are fake using the ampt instead of PvOnAc, We dont have a Pv on Ac at the moment and we don't have it classes :TODO
+ public static AcPowerDevice? CalculateGridBusLoad(EmuMeterRegisters? gridMeter, AmptStatus? pvOnAcGrid, AcPowerDevice? gridBusToIslandBusPower)
+ {
+ var a = gridMeter ?.Ac.Power;
+ var b = pvOnAcGrid is not null? pvOnAcGrid?.Dc.Power.Value: 0;
+ var d = gridBusToIslandBusPower?.Power;
+
+ if (a is null || b is null || d is null)
+ return null;
+
+ var c = a + b - d; // [eq1]
+
+ return new AcPowerDevice { Power = c };
+ }
+
+ //We are fake using the ampt instead of PvOnAc, We dont have a Pv on Ac at the moment and we don't have it classes :TODO
+ public static DcPowerDevice? CalculateAcDcToDcLink(AmptStatus? pvOnDc, DcDcDevicesRecord? dcDc, AcDcDevicesRecord acDc)
+ {
+ var i = pvOnDc?.Dc.Power;
+ var k = dcDc?.Dc.Link.Power; // We don't check on the DcDc because this device is mandatory
+ var g = acDc?.Dc.Power;
+
+ if (i is null || k is null )
+ {
+ return new DcPowerDevice { Power = g };
+ }
+
+ var h = -(i - k); // [eq4]
+
+ return new DcPowerDevice { Power = h };
+ }
+
+ //We are fake using the ampt instead of PvOnAc, We dont have a Pv on Ac at the moment and we don't have it classes :TODO
+ public static AcPowerDevice? CalculateGridBusToIslandBusPower(AmptStatus? pvOnAcIsland, EmuMeterRegisters? loadOnAcIsland, AcDcDevicesRecord? acDc)
+ {
+ var e = pvOnAcIsland is not null? pvOnAcIsland?.Dc.Power.Value: 0;
+ var f = loadOnAcIsland is not null? loadOnAcIsland?.Ac.Power : 0;
+ var g = acDc ?.Ac.Power; // We don't check on the AcDc because this device is mandatory, if this does not exist the system will not start
+
+ if (e is null || f is null || g is null)
+ return null;
+
+ var d = f + g - e; // [eq2]
+
+ return new AcPowerDevice { Power = d };
+ }
+
+ public static DcPowerDevice? CalculateDcLoad(AcDcDevicesRecord? acDc, AmptStatus? pvOnDc, DcDcDevicesRecord? dcDc)
+ {
+ var h = acDc?.Dc.Power; // We don't check on the AcDc because this device is mandatory
+ var i = pvOnDc is not null? pvOnDc?.Dc.Power: 0;
+ var k = dcDc?.Dc.Link.Power; // We don't check on the DcDc because this device is mandatory
+
+ if (h is null || i is null || k is null)
+ return null;
+
+ var j = h + i - k; // [eq3]
+
+ return new DcPowerDevice { Power = j};
+ }
+
+}
diff --git a/csharp/App/SodiStoreMax/tunnelstoSalimaxX.sh b/csharp/App/SodiStoreMax/tunnelstoSalimaxX.sh
new file mode 100755
index 000000000..03b256195
--- /dev/null
+++ b/csharp/App/SodiStoreMax/tunnelstoSalimaxX.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+host="ie-entwicklung@$1"
+
+tunnel() {
+ name=$1
+ ip=$2
+ rPort=$3
+ lPort=$4
+
+ echo -n "$name @ $ip mapped to localhost:$lPort "
+ ssh -nNTL "$lPort:$ip:$rPort" "$host" 2> /dev/null &
+
+ until nc -vz 127.0.0.1 $lPort 2> /dev/null
+ do
+ echo -n .
+ sleep 0.3
+ done
+
+ echo "ok"
+}
+
+echo ""
+
+tunnel "Trumpf Inverter (http) " 10.0.2.1 80 8001
+tunnel "Trumpf DCDC (http) " 10.0.3.1 80 8002
+tunnel "Ext Emu Meter (http) " 10.0.4.1 80 8003
+tunnel "Int Emu Meter (http) " 10.0.4.2 80 8004
+tunnel "AMPT (http) " 10.0.5.1 8080 8005
+
+tunnel "Trumpf Inverter (modbus)" 10.0.2.1 502 5001
+tunnel "Trumpf DCDC (modbus) " 10.0.3.1 502 5002
+tunnel "Ext Emu Meter (modbus) " 10.0.4.1 502 5003
+tunnel "Int Emu Meter " 10.0.4.2 502 5004
+tunnel "AMPT (modbus) " 10.0.5.1 502 5005
+tunnel "Adam " 10.0.1.1 502 5006 #for AMAX is 10.0.1.3
+tunnel "Batteries " 127.0.0.1 6855 5007
+
+echo
+echo "press any key to close the tunnels ..."
+read -r -n 1 -s
+kill $(jobs -p)
+echo "done"
+
diff --git a/csharp/App/SodiStoreMax/uploadBatteryFw/AF0A.bin b/csharp/App/SodiStoreMax/uploadBatteryFw/AF0A.bin
new file mode 100644
index 000000000..e5b7b9aba
Binary files /dev/null and b/csharp/App/SodiStoreMax/uploadBatteryFw/AF0A.bin differ
diff --git a/csharp/App/SodiStoreMax/uploadBatteryFw/update_firmware.sh b/csharp/App/SodiStoreMax/uploadBatteryFw/update_firmware.sh
new file mode 100755
index 000000000..fab8d1174
--- /dev/null
+++ b/csharp/App/SodiStoreMax/uploadBatteryFw/update_firmware.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+dotnet_version='net6.0'
+salimax_ip="$1"
+username='ie-entwicklung'
+root_password='Salimax4x25'
+
+set -e
+
+ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.29")
+battery_ids=("2" "3" "4" "5" "6" "7" "8" "9" "10" "11")
+
+
+for ip_address in "${ip_addresses[@]}"; do
+ scp upload-bms-firmware AF0A.bin "$username"@"$ip_address":/home/"$username"
+ ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl stop battery.service"
+ ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S apt install python3-pip -y"
+ ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S pip3 install pymodbus"
+
+ for battery in "${battery_ids[@]}"; do
+ ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S python3 upload-bms-firmware ttyUSB0 " "$battery" " AF0A.bin"
+ done
+ ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl start battery.service"
+ ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl rm upload-bms-firmware AF0A.bin"
+
+echo "Deployed and ran commands on $ip_address"
+done
+
+
diff --git a/csharp/App/SodiStoreMax/uploadBatteryFw/upload-bms-firmware b/csharp/App/SodiStoreMax/uploadBatteryFw/upload-bms-firmware
new file mode 100755
index 000000000..58d2c804d
--- /dev/null
+++ b/csharp/App/SodiStoreMax/uploadBatteryFw/upload-bms-firmware
@@ -0,0 +1,288 @@
+#!/usr/bin/python2 -u
+# coding=utf-8
+
+import os
+import struct
+from time import sleep
+
+import serial
+from os import system
+import logging
+
+from pymodbus.client import ModbusSerialClient as Modbus
+from pymodbus.exceptions import ModbusIOException
+from pymodbus.pdu import ModbusResponse
+from os.path import dirname, abspath
+from sys import path, argv, exit
+
+path.append(dirname(dirname(abspath(__file__))))
+
+PAGE_SIZE = 0x100
+HALF_PAGE =int( PAGE_SIZE / 2)
+WRITE_ENABLE = [1]
+FIRMWARE_VERSION_REGISTER = 1054
+
+ERASE_FLASH_REGISTER = 0x2084
+RESET_REGISTER = 0x2087
+logging.basicConfig(level=logging.INFO)
+
+
+# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
+# noinspection PyUnreachableCode
+if False:
+ from typing import List, NoReturn, Iterable, Optional
+
+def calc_stm32_crc_round(crc, data):
+ # type: (int, int) -> int
+ crc = crc ^ data
+ for _ in range(32):
+ xor = (crc & 0x80000000) != 0
+ crc = (crc & 0x7FFFFFFF) << 1 # clear bit 31 because python ints have "infinite" bits
+ if xor:
+ crc = crc ^ 0x04C11DB7
+
+ return crc
+
+
+def calc_stm32_crc(data):
+ # type: (Iterable[int]) -> int
+ crc = 0xFFFFFFFF
+
+ for dw in data:
+ crc = calc_stm32_crc_round(crc, dw)
+
+ return crc
+
+
+def init_modbus(tty):
+ # type: (str) -> Modbus
+
+ return Modbus(
+ port='/dev/' + tty,
+ method='rtu',
+ baudrate=115200,
+ stopbits=1,
+ bytesize=8,
+ timeout=0.5, # seconds
+ parity=serial.PARITY_ODD)
+
+
+def failed(response):
+ # type: (ModbusResponse) -> bool
+
+ # Todo 'ModbusIOException' object has no attribute 'function_code'
+ return response.function_code > 0x80
+
+
+def clear_flash(modbus, slave_address):
+ # type: (Modbus, int) -> bool
+
+ print ('erasing flash...')
+
+ write_response = modbus.write_registers(address=0x2084, values=[1], slave=slave_address)
+
+ if failed(write_response):
+ print('erasing flash FAILED')
+ return False
+
+ flash_countdown = 17
+ while flash_countdown > 0:
+ read_response = modbus.read_holding_registers(address=0x2085, count=1, slave=slave_address)
+
+ if failed(read_response):
+ print('erasing flash FAILED')
+ return False
+
+ if read_response.registers[0] != flash_countdown:
+ flash_countdown = read_response.registers[0]
+
+ msg = str(100 * (16 - flash_countdown) / 16) + '%'
+ print('\r{0} '.format(msg), end=' ')
+
+ print('done!')
+
+ return True
+
+
+# noinspection PyShadowingBuiltins
+def bytes_to_words(bytes):
+ # type: (str) -> List[int]
+ return list(struct.unpack('>' + int(len(bytes)/2) * 'H', bytes))
+
+
+def send_half_page_1(modbus, slave_address, data, page):
+ # type: (Modbus, int, str, int) -> NoReturn
+
+ first_half = [page] + bytes_to_words(data[:HALF_PAGE])
+ write_first_half = modbus.write_registers(0x2000, first_half, slave=slave_address)
+
+ if failed(write_first_half):
+ raise Exception("Failed to write page " + str(page))
+
+
+def send_half_page_2(modbus, slave_address, data, page):
+ # type: (Modbus, int, str, int) -> NoReturn
+
+ registers = bytes_to_words(data[HALF_PAGE:]) + calc_crc(page, data) + WRITE_ENABLE
+ result = modbus.write_registers(0x2041, registers, slave=slave_address)
+
+ if failed(result):
+ raise Exception("Failed to write page " + str(page))
+
+
+def get_fw_name(fw_path):
+ # type: (str) -> str
+ return fw_path.split('/')[-1].split('.')[0]
+
+
+def upload_fw(modbus, slave_id, fw_path, fw_name):
+ # type: (Modbus, int, str, str) -> NoReturn
+
+ with open(fw_path, "rb") as f:
+
+ size = os.fstat(f.fileno()).st_size
+ n_pages = int(size / PAGE_SIZE)
+
+ print('uploading firmware ' + fw_name + ' to BMS ...')
+
+ for page in range(0, n_pages):
+ page_data = f.read(PAGE_SIZE)
+
+ msg = "page " + str(page + 1) + '/' + str(n_pages) + ' ' + str(100 * page / n_pages + 1) + '%'
+ print('\r{0} '.format(msg), end=' ')
+
+ if is_page_empty(page_data):
+ continue
+ sleep(0.01)
+ send_half_page_1(modbus, slave_id, page_data, page)
+ sleep(0.01)
+ send_half_page_2(modbus, slave_id, page_data, page)
+
+
+def is_page_empty(page):
+ # type: (str) -> bool
+ return page.count(b'\xff') == len(page)
+
+
+def reset_bms(modbus, slave_id):
+ # type: (Modbus, int) -> bool
+
+ print ('resetting BMS...')
+
+ result = modbus.write_registers(RESET_REGISTER, [1], slave=slave_id)
+
+ # expecting a ModbusIOException (timeout)
+ # BMS can no longer reply because it is already reset
+ success = isinstance(result, ModbusIOException)
+
+ if success:
+ print('done')
+ else:
+ print('FAILED to reset battery!')
+
+ return success
+
+
+def calc_crc(page, data):
+ # type: (int, str) -> List[int]
+
+ crc = calc_stm32_crc([page] + bytes_to_words(data))
+ crc_bytes = struct.pack('>L', crc)
+
+ return bytes_to_words(crc_bytes)
+
+
+def identify_battery(modbus, slave_id):
+ # type: (Modbus, int) -> Optional[str]
+ print("slave id=",slave_id)
+ target = 'battery ' + str(slave_id) + ' at ' + '502'
+
+ try:
+
+ print(('contacting ...'))
+
+ response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, slave=slave_id)
+ fw = '{0:0>4X}'.format(response.registers[0])
+
+ print(('found battery with firmware ' + fw))
+
+ return fw
+
+ except:
+ print(('failed to communicate with '))
+ return None
+
+
+def print_usage():
+ print(('Usage: ' + __file__ + ' '))
+ print(('Example: ' + __file__ + ' ttyUSB0 2 A08C.bin'))
+
+
+def parse_cmdline_args(argv):
+ # type: (List[str]) -> (str, str, str, str)
+
+ def fail_with(msg):
+ print(msg)
+ print_usage()
+ exit(1)
+
+ if len(argv) < 1:
+ fail_with('missing argument for tty device')
+
+ if len(argv) < 2:
+ fail_with('missing argument for battery ID')
+
+ if len(argv) < 3:
+ fail_with('missing argument for firmware')
+
+ return argv[0], int(argv[1]), argv[2], get_fw_name(argv[2])
+
+
+def verify_firmware(modbus, battery_id, fw_name):
+ # type: (Modbus, int, str) -> NoReturn
+
+ fw_verify = identify_battery(modbus, battery_id)
+
+ if fw_verify == fw_name:
+ print('SUCCESS')
+ else:
+ print('FAILED to verify uploaded firmware!')
+ if fw_verify is not None:
+ print('expected firmware version ' + fw_name + ' but got ' + fw_verify)
+
+
+def wait_for_bms_reboot():
+ # type: () -> NoReturn
+
+ # wait 20s for the battery to reboot
+
+ print('waiting for BMS to reboot...')
+
+ for t in range(20, 0, -1):
+ print('\r{0} '.format(t), end=' ')
+ sleep(1)
+
+ print('0')
+
+
+def main(argv):
+ # type: (List[str]) -> NoReturn
+
+ tty, battery_id, fw_path, fw_name = parse_cmdline_args(argv)
+ with init_modbus(tty) as modbus:
+
+ if identify_battery(modbus, battery_id) is None:
+ return
+
+ clear_flash(modbus, battery_id)
+ upload_fw(modbus, battery_id, fw_path, fw_name)
+
+ if not reset_bms(modbus, battery_id):
+ return
+
+ wait_for_bms_reboot()
+
+ verify_firmware(modbus, battery_id, fw_name)
+
+
+main(argv[1:])
diff --git a/csharp/InnovEnergy.sln b/csharp/InnovEnergy.sln
index a334ae2c0..291c6023b 100644
--- a/csharp/InnovEnergy.sln
+++ b/csharp/InnovEnergy.sln
@@ -97,6 +97,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BatteryDeligreen", "Lib\Dev
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeligreenBatteryCommunication", "App\DeligreenBatteryCommunication\DeligreenBatteryCommunication.csproj", "{11ED6871-5B7D-462F-8710-B5D85DEC464A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SodiStoreMax", "App\SodiStoreMax\SodiStoreMax.csproj", "{39B83793-49DB-4940-9C25-A7F944607407}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -256,6 +258,10 @@ Global
{11ED6871-5B7D-462F-8710-B5D85DEC464A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11ED6871-5B7D-462F-8710-B5D85DEC464A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11ED6871-5B7D-462F-8710-B5D85DEC464A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {39B83793-49DB-4940-9C25-A7F944607407}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {39B83793-49DB-4940-9C25-A7F944607407}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {39B83793-49DB-4940-9C25-A7F944607407}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {39B83793-49DB-4940-9C25-A7F944607407}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A}
@@ -300,5 +306,6 @@ Global
{F2967439-A590-4D5E-9208-1B973C83AA1C} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{1045AC74-D4D8-4581-AAE3-575DF26060E6} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{11ED6871-5B7D-462F-8710-B5D85DEC464A} = {145597B4-3E30-45E6-9F72-4DD43194539A}
+ {39B83793-49DB-4940-9C25-A7F944607407} = {145597B4-3E30-45E6-9F72-4DD43194539A}
EndGlobalSection
EndGlobal
diff --git a/csharp/Lib/Devices/Adam6360D/Doc/flowchart_LEDSetting_20241216_ps.odp b/csharp/Lib/Devices/Adam6360D/Doc/flowchart_LEDSetting_20241216_ps.odp
new file mode 100644
index 000000000..25ad21802
Binary files /dev/null and b/csharp/Lib/Devices/Adam6360D/Doc/flowchart_LEDSetting_20241216_ps.odp differ
diff --git a/csharp/Lib/Devices/BatteryDeligreen/Alarms.cs b/csharp/Lib/Devices/BatteryDeligreen/Alarms.cs
new file mode 100644
index 000000000..efb206a46
--- /dev/null
+++ b/csharp/Lib/Devices/BatteryDeligreen/Alarms.cs
@@ -0,0 +1,21 @@
+using InnovEnergy.Lib.Units;
+
+namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
+
+
+public class Alarms
+{
+ public struct CellAlarm
+ {
+ public Int32 CellNumber { get; set; }
+ public String AlarmDescription { get; set; }
+ }
+
+ private static readonly Dictionary ByteAlarmCodes = new Dictionary
+ {
+ { "00", "Normal, no alarm" },
+ { "01", "Alarm that analog quantity reaches the lower limit" },
+ { "02", "Alarm that analog quantity reaches the upper limit" },
+ { "F0", "Other alarms" }
+ };
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenAlarmRecord.cs b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenAlarmRecord.cs
new file mode 100644
index 000000000..7b860cfa1
--- /dev/null
+++ b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenAlarmRecord.cs
@@ -0,0 +1,26 @@
+namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
+
+public class BatteryDeligreenAlarmRecord
+{/*
+ public String FwVersion { get; set; }
+
+ public TemperaturesList TemperaturesList { get; set; }
+ // public Dc_ Dc { get; set; }
+
+ public BatteryDeligreenAlarmRecord(Voltage busVoltage, Current busCurrent ,String fwVersion, Percent soc, UInt16 numberOfCycles, Double batteryCapacity, Double ratedCapacity, Voltage totalBatteryVoltage, Percent soh, Double residualCapacity, List cellVoltage, TemperaturesList temperaturesList)
+ {
+ BusVoltage = busVoltage;
+ BusCurrent = busCurrent;
+ FwVersion = fwVersion;
+ TotalBatteryVoltage = totalBatteryVoltage;
+ ResidualCapacity = residualCapacity;
+ BatteryCapacity = batteryCapacity;
+ Soc = soc;
+ RatedCapacity = ratedCapacity;
+ NumberOfCycles = numberOfCycles;
+ Soh = soh;
+ CellVoltage = cellVoltage;
+ TemperaturesList = temperaturesList;
+ Power = busVoltage * busCurrent;
+ }*/
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDataRecord.cs b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDataRecord.cs
new file mode 100644
index 000000000..590214524
--- /dev/null
+++ b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDataRecord.cs
@@ -0,0 +1,54 @@
+using InnovEnergy.Lib.Units.Power;
+using static InnovEnergy.Lib.Devices.BatteryDeligreen.Temperatures;
+
+namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
+using InnovEnergy.Lib.Units;
+
+using Strings = IReadOnlyList;
+
+public class BatteryDeligreenDataRecord
+{
+
+ // public Strings Warnings => ParseWarnings().OrderBy(w => w).ToList();
+ // public Strings Alarms => ParseAlarms() .OrderBy(w => w).ToList();
+
+ public String FwVersion { get; set; }
+ public Voltage BusVoltage { get; set; }
+ public Current BusCurrent { get; set; }
+ public ActivePower Power { get; set; }
+ public Voltage TotalBatteryVoltage { get; set; }
+ public Double ResidualCapacity { get; set; }
+ public Double BatteryCapacity { get; set; }
+ public Percent Soc { get; set; }
+ public Double RatedCapacity { get; set; }
+ public UInt16 NumberOfCycles { get; set; }
+ public Percent Soh { get; set; }
+ public List CellVoltage { get; set; }
+ public TemperaturesList TemperaturesList { get; set; }
+ // public Dc_ Dc { get; set; }
+
+ public BatteryDeligreenDataRecord(Voltage busVoltage, Current busCurrent ,String fwVersion, Percent soc, UInt16 numberOfCycles, Double batteryCapacity, Double ratedCapacity, Voltage totalBatteryVoltage, Percent soh, Double residualCapacity, List cellVoltage, TemperaturesList temperaturesList)
+ {
+ BusVoltage = busVoltage;
+ BusCurrent = busCurrent;
+ FwVersion = fwVersion;
+ TotalBatteryVoltage = totalBatteryVoltage;
+ ResidualCapacity = residualCapacity;
+ BatteryCapacity = batteryCapacity;
+ Soc = soc;
+ RatedCapacity = ratedCapacity;
+ NumberOfCycles = numberOfCycles;
+ Soh = soh;
+ CellVoltage = cellVoltage;
+ TemperaturesList = temperaturesList;
+ Power = busVoltage * busCurrent;
+ }
+
+ // public struct Dc_
+ // {
+ // public Voltage Voltage => BusVoltage;
+ // public Current Current => BusCurrent;
+ // public ActivePower Power => BusVoltage * BusCurrent;
+
+ // }
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevice.cs b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevice.cs
index 2b590d796..5bd7118ff 100644
--- a/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevice.cs
+++ b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevice.cs
@@ -1,40 +1,65 @@
-namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
-
-using System;
using System.IO.Ports;
+using InnovEnergy.Lib.Utils;
+namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
public class BatteryDeligreenDevice
{
- private const Parity Parity = System.IO.Ports.Parity.None;
+ private const Parity Parity = System.IO.Ports.Parity.None;
private const StopBits StopBits = System.IO.Ports.StopBits.One;
- private const Int32 BaudRate = 19200;
- private const Int32 DataBits = 8;
+ private const Int32 BaudRate = 19200;
+ private const Int32 DataBits = 8;
private readonly SerialPort _serialPort;
- // Constructor for local serial port connection
- public BatteryDeligreenDevice(String tty)
- {
- _serialPort = new SerialPort(tty, BaudRate, Parity, DataBits, StopBits)
- {
- ReadTimeout = 1000, // 1 second timeout for reads
- WriteTimeout = 1000 // 1 second timeout for writes
- };
+ public UInt16 SlaveId { get; }
- try
+ // Dynamically construct the frame to send
+ private const String FrameStart = "7E"; // Starting of the frame
+ private const String Version = "3230"; // Protocol version
+ private const String DeviceCode = "3436"; // Device Code
+ private const String TelemetryFunctionCode = "3432";
+ private const String TelcommandFunctionCode = "3434";
+
+ private static SerialPort _sharedSerialPort;
+
+ private static SerialPort GetSharedPort(string tty)
+ {
+ if (_sharedSerialPort == null)
{
- // Open the serial port
- _serialPort.Open();
- Console.WriteLine("Serial port opened successfully.");
+ _sharedSerialPort = new SerialPort(tty, BaudRate, Parity, DataBits, StopBits)
+ {
+ ReadTimeout = 1000, // 1 second timeout for reads
+ WriteTimeout = 1000 // 1 second timeout for writes
+ };
+ try
+ {
+ // Open the shared serial port
+ _sharedSerialPort.Open();
+ Console.WriteLine("Shared Serial Port opened successfully.");
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
}
- catch (Exception e)
- {
- Console.WriteLine(e);
- }
-
+
+ return _sharedSerialPort;
}
-
+
+ public static void CloseSharedPort()
+ {
+ _sharedSerialPort?.Close();
+ Console.WriteLine("Shared Serial Port closed.");
+ }
+
+ // Constructor for local serial port connection
+ public BatteryDeligreenDevice(String tty, UInt16 slaveId)
+ {
+ SlaveId = slaveId;
+ _serialPort = GetSharedPort(tty);
+ }
+
// Method to send data to the device
private void Write(String hexCommand)
{
@@ -45,7 +70,7 @@ public class BatteryDeligreenDevice
// Send the command
_serialPort.Write(commandBytes, 0, commandBytes.Length);
- Console.WriteLine("Command sent successfully.");
+ //Console.WriteLine("Write Command sent successfully.");
}
catch (TimeoutException)
{
@@ -67,7 +92,7 @@ public class BatteryDeligreenDevice
var buffer = new Byte[bufferSize];
var bytesRead = _serialPort.Read(buffer, 0, bufferSize);
- Console.WriteLine($"Read {bytesRead} bytes from the device.");
+ //Console.WriteLine($"Read {bytesRead} bytes from the device.");
// Return only the received bytes
var responseData = new Byte[bytesRead];
@@ -90,7 +115,7 @@ public class BatteryDeligreenDevice
{
return BitConverter.ToString(byteArray).Replace("-", "").ToUpper();
}
-
+
// Helper method to convert a hex string to a byte array
private static Byte[] HexStringToByteArray(string hex)
{
@@ -102,6 +127,7 @@ public class BatteryDeligreenDevice
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
+
return bytes;
}
@@ -114,29 +140,37 @@ public class BatteryDeligreenDevice
Console.WriteLine("Serial port closed.");
}
}
-
- // Read telemetry data from the connected device
- public async Task ReadTelemetryData()
- {
- const String frameToSend = "7E3230303034363432453030323030464433370D"; // Example custom frame
+ // Read telemetry data from the connected device
+ private async Task ReadTelemetryData(UInt16 batteryId)
+ {
+ String frameToSend = batteryId switch
+ {
+ 0 => "7E3230303034363432453030323030464433370D",
+ 1 => "7E3230303134363432453030323031464433350D",
+ 2 => "7E3230303234363432453030323032464433330D",
+ 3 => "7E3230303334363432453030323033464433310D",
+ 4 => "7E3230303434363432453030323034464432460D",
+ 5 => "7E3230303534363432453030323035464432440D",
+ 6 => "7E3230303634363432453030323036464432420D",
+ 7 => "7E3230303734363432453030323037464432390D",
+ 8 => "7E3230303834363432453030323038464432370D",
+ 9 => "7E3230303934363432453030323039464432350D",
+ _ => "0"
+ };
+ // var frameToSend = ConstructFrameToSend(batteryId,TelemetryFunctionCode);
+
try
{
// Write the frame to the channel (send it to the device)
- await Task.Run(() => Write(frameToSend));
-
+ Write(frameToSend);
// Read the response from the channel (assuming max response size)
- var responseBytes = await Task.Run(() => Read(1024)); // Assuming Read can be executed asynchronously
+ var responseBytes = await ReadFullResponse(168, 64);
// Convert the byte array to a hexadecimal string
var responseHex = BytesToHexString(responseBytes);
-
- new TelemetryFrameParser().ParsingTelemetryFrame(responseHex);
-
- // Parse the ASCII response (you can implement any custom parsing logic)
- var responseData = ParseAsciiResponse(responseBytes.ToArray());
- return responseData;
+ return new TelemetryFrameParser().ParsingTelemetryFrame(responseHex);
}
catch (Exception ex)
{
@@ -145,34 +179,83 @@ public class BatteryDeligreenDevice
}
}
- public Byte[] ReadTelecomandData()
+ private Task ReadFullResponse(Int32 totalBytes, Int32 chunkSize)
{
- const String frameToSend = "7E3230303034363434453030323030464433350D"; // Example custom frame
-
- // Write the frame to the channel (send it to the device)
- Write(frameToSend);
+ var responseBuffer = new List();
+ while (responseBuffer.Count < totalBytes)
+ {
+ // Calculate how many more bytes need to be read
+ var bytesToRead = Math.Min(chunkSize, totalBytes - responseBuffer.Count);
+ var chunk = Read(bytesToRead);
- // Read the response from the channel (assuming max response size)
- var responseBytes = Read(1024); // Adjust this size if needed
+ if (chunk.Length == 0)
+ {
+ throw new TimeoutException("Failed to read the expected number of bytes from the device.");
+ }
- // Parse the ASCII response (you can implement any custom parsing logic)
- var responseData = ParseAsciiResponse(responseBytes.ToArray());
+ responseBuffer.AddRange(chunk);
+ }
- return responseData;
+ return Task.FromResult(responseBuffer.ToArray());
}
- // Helper method to parse the ASCII response (you can add any parsing logic here)
- private static byte[] ParseAsciiResponse(byte[] responseBytes)
+ private async Task ReadTelecomandData(UInt16 batteryId)
{
- Console.WriteLine($"Last Timestamp: {DateTime.Now:HH:mm:ss.fff}");
- // Convert the byte array to a hex string for display
- var hexResponse = BitConverter.ToString(responseBytes).Replace("-", " ");
- //Console.WriteLine($"Response (Hex): {hexResponse}");
- // Implement custom parsing logic if necessary based on the protocol's frame
- // For now, we return the raw response bytes
- return responseBytes;
+ var frameToSend = batteryId switch
+ {
+ 0 => "7E3230303034363434453030323030464433350D",
+ 1 => "7E3230303134363434453030323031464433330D",
+ 2 => "7E3230303234363434453030323032464433310D",
+ 3 => "7E3230303334363434453030323033464432460D",
+ 4 => "7E3230303434363434453030323034464432440D",
+ 5 => "7E3230303534363434453030323035464432420D",
+ 6 => "7E3230303634363434453030323036464432390D",
+ 7 => "7E3230303734363434453030323037464432370D",
+ 8 => "7E3230303834363434453030323038464432350D",
+ 9 => "7E3230303934363434453030323039464432330D",
+ _ => "0"
+ };
+ try
+ {
+ // Write the frame to the channel (send it to the device)
+ Write(frameToSend);
+ // await Task.Delay(delayFrame2);
+ // Read the response from the channel (assuming max response size)
+ var responseBytes = await ReadFullResponse(116, 64); // Assuming Read can be executed asynchronously
+ // Convert the byte array to a hexadecimal string
+ var responseHex = BytesToHexString(responseBytes);
+
+ var response = new TelecommandFrameParser().ParsingTelecommandFrame(responseHex);
+
+ return new BatteryDeligreenAlarmRecord();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error during Telecomnd data retrieval: {ex.Message}");
+ throw;
+ }
}
-}
+ public async Task Reads()
+ {
+ var dataRecord = ReadTelemetryData(SlaveId).Result;
+ var alarmRecord = ReadTelecomandData(SlaveId).Result;
+ await Task.Delay(5); // looks like this is need. A time delay needed between each frame to send to each battery
+ return dataRecord != null ? new BatteryDeligreenRecord(dataRecord, alarmRecord) : null;
+ }
+ private static String ConstructFrameToSend(UInt16 batteryId, String functionCode)
+ {
+ // Convert batteryId to a 2-character ASCII string
+ var batteryIdAscii = $"{(Char)(batteryId / 10 + '0')}{(char)(batteryId % 10 + '0')}";
+ var batteryIdHex = string.Concat(batteryIdAscii.Select(c => ((Int32)c).ToString("X2")));
+ Console.WriteLine("Battery ID " + batteryIdHex);
+
+ var frameToSend =
+ FrameStart + Version + batteryIdHex + DeviceCode + functionCode +
+ "453030323030464433370D"; // Example custom frame with dynamic batteryId
+ Console.WriteLine(frameToSend);
+ return frameToSend;
+ }
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevices.cs b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevices.cs
new file mode 100644
index 000000000..c2ec5d6d0
--- /dev/null
+++ b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevices.cs
@@ -0,0 +1,35 @@
+using InnovEnergy.Lib.Utils;
+
+namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
+
+public class BatteryDeligreenDevices
+{
+ private readonly IReadOnlyList _devices;
+
+ public BatteryDeligreenDevices(IReadOnlyList devices) => _devices = devices;
+
+ public BatteryDeligreenRecords? Read()
+ {
+ var records = _devices
+ .Select(TryRead)
+ .NotNull()
+ .ToList();
+
+ return BatteryDeligreenRecords.FromBatteries(records);
+ }
+
+ private static BatteryDeligreenRecord? TryRead(BatteryDeligreenDevice d)
+ {
+ try
+ {
+ return d.Reads().Result;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Failed to read Battery node {d.SlaveId}\n{e.Message}");
+ // TODO: log
+
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenRecord.cs b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenRecord.cs
new file mode 100644
index 000000000..d26c53924
--- /dev/null
+++ b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenRecord.cs
@@ -0,0 +1,13 @@
+namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
+
+public class BatteryDeligreenRecord
+{
+ public readonly BatteryDeligreenDataRecord BatteryDeligreenDataRecord;
+ public readonly BatteryDeligreenAlarmRecord BatteryDeligreenAlarmRecord;
+
+ public BatteryDeligreenRecord(BatteryDeligreenDataRecord batteryDeligreenDataRecord, BatteryDeligreenAlarmRecord batteryDeligreenAlarmRecord)
+ {
+ BatteryDeligreenDataRecord = batteryDeligreenDataRecord;
+ BatteryDeligreenAlarmRecord = batteryDeligreenAlarmRecord;
+ }
+}
diff --git a/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenRecords.cs b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenRecords.cs
new file mode 100644
index 000000000..3dbcf9245
--- /dev/null
+++ b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenRecords.cs
@@ -0,0 +1,46 @@
+using InnovEnergy.Lib.Units;
+using InnovEnergy.Lib.Units.Composite;
+using InnovEnergy.Lib.Units.Power;
+
+namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
+
+public class BatteryDeligreenRecords
+{
+ //public required DcBus Dc { get; init; }
+ public required Current Current { get; init; }
+ public required Voltage Voltage { get; init; }
+ public required Percent Soc { get; init; }
+ public required Double Soh { get; init; }
+ public required Percent CurrentMinSoc { get; init; }
+ public required Temperature TemperatureCell1 { get; init; }
+ public required Double Power { get; init; }
+
+ // public required Temperature TemperatureCell2 { get; init; }
+ // public required Temperature TemperatureCell3 { get; init; }
+ // public required Temperature TemperatureCell4 { get; init; }
+ // to continue other temperature
+
+ public required IReadOnlyList Devices { get; init; }
+
+ public static BatteryDeligreenRecords? FromBatteries(IReadOnlyList? records)
+ {
+ if (records is null || records.Count == 0)
+ {
+ Console.WriteLine("FromBatteries: either record is null or empty");
+ return null;
+ }
+
+ return new BatteryDeligreenRecords
+ {
+ Devices = records,
+ Soc = records.Average(r => r.BatteryDeligreenDataRecord.Soc.Value),
+ Soh = records.Average(r => r.BatteryDeligreenDataRecord.Soh),
+ CurrentMinSoc = records.Min(r => r.BatteryDeligreenDataRecord.Soc.Value),
+ TemperatureCell1 = records.Average(b => b.BatteryDeligreenDataRecord.TemperaturesList.CellTemperature1),
+ Current = records.Sum(r =>r.BatteryDeligreenDataRecord.BusCurrent),
+ Voltage = records.Average(r =>r.BatteryDeligreenDataRecord.BusVoltage),
+ Power = records.Sum(r => r.BatteryDeligreenDataRecord.Power),
+
+ };
+ }
+}
\ No newline at end of file
diff --git a/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve Telecommand.py b/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve Telecommand.py
new file mode 100644
index 000000000..617e6f724
--- /dev/null
+++ b/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve Telecommand.py
@@ -0,0 +1,480 @@
+import serial
+import csv
+
+TELECOMMAND_FILE_PATH = "Telecommand_Return_Record.csv"
+
+# Table 3
+CID1_DEVICE_CODES = {
+ "46": "Lithium iron phosphate battery BMS",
+}
+
+# Table 4
+CID2_COMMAND_CODES = {
+ "42": "Acquisition of telemetering information",
+ "44": "Acquisition of telecommand information",
+ "45": "Telecontrol command",
+ "47": "Acquisition of teleregulation information",
+ "49": "Setting of teleregulation information",
+ "4F": "Acquisition of the communication protocol version number",
+ "51": "Acquisition of device vendor information",
+ "4B": "Acquisition of historical data",
+ "4D": "Acquisition time",
+ "4E": "Synchronization time",
+ "A0": "Production calibration",
+ "A1": "Production setting",
+ "A2": "Regular recording"
+}
+
+# Table 5
+CID2_RETURN_CODES = {
+ "00": "Normal",
+ "01": "VER error",
+ "02": "CHKSUM error",
+ "03": "LCHKSUM error",
+ "04": "CID2 invalid",
+ "05": "Command format error",
+ "06": "Data invalid (parameter setting)",
+ "07": "No data (history)",
+ "E1": "CID1 invalid",
+ "E2": "Command execution failure",
+ "E3": "Device fault",
+ "E4": "Invalid permissions"
+ }
+
+
+# Table 12
+BYTE_ALARM_CODES = {
+ "00": "Normal, no alarm",
+ "01": "Alarm that analog quantity reaches the lower limit",
+ "02": "Alarm that analog quantity reaches the upper limit",
+ "F0": "Other alarms"
+}
+
+# Table 13
+BIT_ALARM_CODES = {
+ "Alarm event 1": (
+ "Voltage sensor fault",
+ "Temperature sensor fault",
+ "Current sensor fault",
+ "Key switch fault",
+ "Cell voltage dropout fault",
+ "Charge switch fault",
+ "Discharge switch fault",
+ "Current limit switch fault"
+ ),
+ "Alarm event 2": (
+ "Monomer high voltage alarm",
+ "Monomer overvoltage protection",
+ "Monomer low voltage alarm",
+ "Monomer under voltage protection",
+ "High voltage alarm for total voltage",
+ "Overvoltage protection for total voltage",
+ "Low voltage alarm for total voltage",
+ "Under voltage protection for total voltage"
+ ),
+ "Alarm event 3": (
+ "Charge high temperature alarm",
+ "Charge over temperature protection",
+ "Charge low temperature alarm",
+ "Charge under temperature protection",
+ "Discharge high temperature alarm",
+ "Discharge over temperature protection",
+ "Discharge low temperature alarm",
+ "Discharge under temperature protection"
+ ),
+ "Alarm event 4": (
+ "Environment high temperature alarm",
+ "Environment over temperature protection",
+ "Environment low temperature alarm",
+ "Environment under temperature protection",
+ "Power over temperature protection",
+ "Power high temperature alarm",
+ "Cell low temperature heating",
+ "Reservation bit"
+ ),
+ "Alarm event 5": (
+ "Charge over current alarm",
+ "Charge over current protection",
+ "Discharge over current alarm",
+ "Discharge over current protection",
+ "Transient over current protection",
+ "Output short circuit protection",
+ "Transient over current lockout",
+ "Output short circuit lockout"
+ ),
+ "Alarm event 6": (
+ "Charge high voltage protection",
+ "Intermittent recharge waiting",
+ "Residual capacity alarm",
+ "Residual capacity protection",
+ "Cell low voltage charging prohibition",
+ "Output reverse polarity protection",
+ "Output connection fault",
+ "Inside bit"
+ ),
+ "On-off state": (
+ "Discharge switch state",
+ "Charge switch state",
+ "Current limit switch state",
+ "Heating switch state",
+ "Reservation bit",
+ "Reservation bit",
+ "Reservation bit",
+ "Reservation bit"
+ ),
+ "Equilibrium state 1": (
+ "Cell 01 equilibrium",
+ "Cell 02 equilibrium",
+ "Cell 03 equilibrium",
+ "Cell 04 equilibrium",
+ "Cell 05 equilibrium",
+ "Cell 06 equilibrium",
+ "Cell 07 equilibrium",
+ "Cell 08 equilibrium"
+ ),
+ "Equilibrium state 2": (
+ "Cell 09 equilibrium",
+ "Cell 10 equilibrium",
+ "Cell 11 equilibrium",
+ "Cell 12 equilibrium",
+ "Cell 13 equilibrium",
+ "Cell 14 equilibrium",
+ "Cell 15 equilibrium",
+ "Cell 16 equilibrium"
+ ),
+ "System state": (
+ "Discharge",
+ "Charge",
+ "Floating charge",
+ "Reservation bit",
+ "Standby",
+ "Shutdown",
+ "Reservation bit",
+ "Reservation bit"
+ ),
+ "Disconnection state 1": (
+ "Cell 01 disconnection",
+ "Cell 02 disconnection",
+ "Cell 03 disconnection",
+ "Cell 04 disconnection",
+ "Cell 05 disconnection",
+ "Cell 06 disconnection",
+ "Cell 07 disconnection",
+ "Cell 08 disconnection"
+ ),
+ "Disconnection state 2": (
+ "Cell 09 disconnection",
+ "Cell 10 disconnection",
+ "Cell 11 disconnection",
+ "Cell 12 disconnection",
+ "Cell 13 disconnection",
+ "Cell 14 disconnection",
+ "Cell 15 disconnection",
+ "Cell 16 disconnection"
+ ),
+ "Alarm event 7": (
+ "Inside bit",
+ "Inside bit",
+ "Inside bit",
+ "Inside bit",
+ "Automatic charging waiting",
+ "Manual charging waiting",
+ "Inside bit",
+ "Inside bit"
+ ),
+ "Alarm event 3": (
+ "EEP storage fault",
+ "RTC error",
+ "Voltage calibration not performed",
+ "Current calibration not performed",
+ "Zero calibration not performed",
+ "Inside bit",
+ "Inside bit",
+ "Inside bit"
+ ),
+}
+
+
+def parse_start_code(frame):
+ soi = frame[0:1]
+ if soi == "~":
+ return "ok!"
+ else:
+ raise ValueError(f"Invalid start identifier! ({soi})")
+
+def parse_version_code(frame):
+ ver = frame[1:3]
+ return f"Protocol Version V{ver[0]}.{ver[1]}"
+
+def parse_address_code(frame):
+ adr = frame[3:5]
+ if 0 <= int(adr) <= 15:
+ return adr
+ else:
+ raise ValueError(f"Invalid address: {adr} (out of range 0-15)")
+
+def parse_device_code(frame):
+ cid1 = frame[5:7]
+ return CID1_DEVICE_CODES.get(cid1, "Unknown!")
+
+def parse_function_code(frame):
+ cid2 = frame[7:9]
+ if cid2 in CID2_COMMAND_CODES:
+ return f"Command -> {CID2_COMMAND_CODES.get(cid2)}"
+ elif cid2 in CID2_RETURN_CODES:
+ return f"Return -> {CID2_RETURN_CODES.get(cid2)}"
+ else:
+ return f"Unknown CID2: {cid2}"
+
+def parse_lchksum(length_code):
+ # implements chapter 3.2.2 of the Protocol Specification
+ lchksum = int(length_code[0], 16)
+ # Compute lchksum
+ d11d10d09d08 = int(length_code[1])
+ d07d06d05d04 = int(length_code[2])
+ d03d0ld01d00 = int(length_code[3])
+ sum = d11d10d09d08 + d07d06d05d04 + d03d0ld01d00
+ remainder = sum % 16
+ inverted = ~remainder & 0xF
+ computed_lchksum = (inverted + 1) & 0xF
+ if computed_lchksum == lchksum:
+ return "ok!"
+ else:
+ raise ValueError(f"Invalid LCHKSUM: {lchksum} (computed: {computed_lchksum})")
+
+def parse_lenid(length_code):
+ # implements chapter 3.2.1 of the Protocol Specification
+ d11d10d09d08 = int(length_code[1])
+ d07d06d05d04 = int(length_code[2])
+ d03d0ld01d00 = int(length_code[3])
+ lenid = d11d10d09d08 << 8 | d07d06d05d04 << 4 | d03d0ld01d00
+ return lenid >> 1
+
+def parse_length_code(frame):
+ # implements chapter 3.2 of the Protocol Specification
+ length_code = frame[9:13]
+ lchksum = parse_lchksum(length_code)
+ lenid = parse_lenid(length_code)
+ return { "LCHKSUM": lchksum, "LENID": lenid }
+
+def parse_info(frame):
+ cid2 = frame[7:9]
+ lenid = parse_lenid(frame[9:13])
+ info = frame[13:13+lenid*2]
+
+ if cid2 == '00' and lenid == 49:
+ return parse_telecommand_return(info)
+ elif cid2 == '00' and lenid == 75:
+ return parse_telemetry_return(info)
+ else:
+ return info
+
+def parse_telecommand_return(info_raw, info={}, index=0):
+
+ info["DATA FLAG"] = info_raw[index:index+2]
+ index += 2
+
+ info["COMMAND GROUP"] = info_raw[index:index+2]
+ index += 2
+
+ num_of_cells = int(info_raw[index:index+2], 16)
+ info["Number of cells"] = num_of_cells
+ index += 2
+
+ for cell in range(info["Number of cells"]):
+ alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
+ info[f"Cell {cell +1} alarm"] = alarm
+ index += 2
+
+ num_of_temperatures = int(info_raw[index:index+2], 16)
+ info["Number of temperatures"] = num_of_temperatures
+ index += 2
+
+ for sensor in range(4):
+ alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
+ info[f"Cell temperature alarm {sensor}"] = alarm
+ index += 2
+
+ alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
+ info["Environment temperature alarm"] = alarm
+ index += 2
+
+ alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
+ info["Power temperature alarm 1"] = alarm
+ index += 2
+
+ alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
+ info["Charge/discharge current alarm"] = alarm
+ index += 2
+
+ alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2])
+ info["Total battery voltage alarm"] = alarm
+ index += 2
+
+ num_custom = int(info_raw[index:index+2], 16)
+ info["Number of custom alarms"] = num_custom
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Alarm event 1"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Alarm event 2"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Alarm event 3"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Alarm event 4"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Alarm event 5"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Alarm event 6"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["On-off state"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Equilibrium state 1"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Equilibrium state 2"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["System state"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Disconnection state 1"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Disconnection state 2"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Alarm event 7"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Alarm event 8"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Reservation extention 1"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Reservation extention 2"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Reservation extention 3"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Reservation extention 4"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Reservation extention 5"] = alarm
+ index += 2
+
+ alarm = info_raw[index:index+2]
+ info["Reservation extention 6"] = alarm
+ index += 2
+
+ save_dict_to_csv(TELECOMMAND_FILE_PATH, info)
+ return f"Telecommand Return Data saved in ./{TELECOMMAND_FILE_PATH}"
+
+
+def save_dict_to_csv(file_path, data):
+ with open(file_path, mode='a+', newline='') as csvfile:
+ csvfile.seek(0)
+ has_header = csvfile.read(1) != ""
+ csvfile.seek(0, 2)
+ writer = csv.DictWriter(csvfile, fieldnames=data.keys())
+ if not has_header:
+ writer.writeheader()
+ writer.writerow(data)
+
+def parse_checksum(frame):
+ """implements section 3.3 of the Protocol Specification"""
+ chksum = int(frame[-6:-1], 16)
+ data = frame[1:-5]
+ # Compute chksum
+ ascii_sum = sum(ord(char) for char in data)
+ remainder = ascii_sum % 65536
+ inverted = ~remainder & 0xFFFF
+ computed_chksum = (inverted + 1) & 0xFFFF
+ # Compare with CHKSUM in frame
+ if computed_chksum == chksum:
+ return "ok!"
+ else:
+ raise ValueError(f"Invalid CHKSUM: {chksum} (computed: {computed_chksum})")
+
+def parse_end_code(frame):
+ eoi = frame[-1]
+ if eoi == "\r":
+ return "ok!"
+ else:
+ raise ValueError(f"Invalid end identifier! ({eoi})")
+
+def parse_modbus_ascii_frame(frame, parsed_data = {}):
+ frame = bytes.fromhex(frame).decode('ascii')
+ parsed_data["SOI"] = parse_start_code(frame)
+ parsed_data["VER"] = parse_version_code(frame)
+ parsed_data["ADR"] = parse_address_code(frame)
+ parsed_data["CID1"] = parse_device_code(frame)
+ parsed_data["CID2"] = parse_function_code(frame)
+ parsed_data["LENGTH"] = parse_length_code(frame)
+ parsed_data["INFO"] = parse_info(frame)
+ parsed_data["CHKSUM"] = parse_checksum(frame)
+ parsed_data["EOI"] = parse_end_code(frame)
+ return parsed_data
+
+def send_command():
+
+ # Define the serial port and baud rate
+ port = 'COM9' # Replace with your actual port
+ baudrate = 19200 # Replace with the correct baud rate for your BMS
+
+ # Create the serial connection
+ try:
+ with serial.Serial(port, baudrate, timeout=1) as ser:
+ # Convert the hex string to bytes
+ command = bytes.fromhex("7E3230303034363434453030323030464433350D")
+
+ # Send the command
+ ser.write(command)
+ print("Command sent successfully.")
+
+ # Wait for and read the response
+ response = ser.read(200) # Adjust the number of bytes to read as needed
+ if response:
+ hex_response = response.hex()
+ print("Response received:", hex_response)
+ # Process the response to check details
+ parsed_result = parse_modbus_ascii_frame(hex_response)
+ for key, value in parsed_result.items():
+ print(f"{key}: {value}")
+ else:
+ print("No response received.")
+ except serial.SerialException as e:
+ print(f"Error opening serial port: {e}")
+ except Exception as e:
+ print(f"An unexpected error occurred: {e}")
+
+if __name__ == "__main__":
+ send_command()
diff --git a/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve Telemetry.py b/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve Telemetry.py
new file mode 100644
index 000000000..33c38b5d1
--- /dev/null
+++ b/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve Telemetry.py
@@ -0,0 +1,277 @@
+import serial
+
+def check_starting_byte_and_extract_details(response):
+ # Ensure the response is a valid hex string
+ if not response or len(response) < 38 + (16 * 8) + 4 + (8 * 14) + 4: # Update minimum length check
+ print("Response is too short to contain valid data.")
+ return
+
+ # Extract the first byte and check if it's '7E'
+ starting_byte = response[:2]
+ if starting_byte.upper() == "7E":
+ print(f"Starting byte: {starting_byte} (Hex)")
+ else:
+ print(f"Incorrect starting byte: {starting_byte}")
+ return
+
+ # Extract the next two bytes for the firmware version
+ version_bytes = response[2:6]
+ try:
+ version_ascii = bytes.fromhex(version_bytes).decode('ascii')
+ print(f"Firmware version: {version_bytes} (Hex), ASCII: {version_ascii}")
+ except ValueError:
+ print(f"Failed to decode firmware version from bytes: {version_bytes}")
+ return
+
+ # Extract the next two bytes for the address
+ address_bytes = response[6:10]
+ try:
+ address_ascii = bytes.fromhex(address_bytes).decode('ascii')
+ address_decimal = int(address_ascii, 16)
+ print(f"Device Address: {address_bytes} (Hex), ASCII: {address_ascii}, Decimal: {address_decimal}")
+ except ValueError:
+ print(f"Failed to decode device address from bytes: {address_bytes}")
+ return
+
+ # Extract the next two bytes for CID1 (Device Code)
+ cid1_bytes = response[10:14]
+ try:
+ cid1_ascii = bytes.fromhex(cid1_bytes).decode('ascii')
+ cid1_decimal = int(cid1_ascii, 16)
+ print(f"Device Code (CID1): {cid1_bytes} (Hex), ASCII: {cid1_ascii}, Decimal: {cid1_decimal}")
+ except ValueError:
+ print(f"Failed to decode device code from bytes: {cid1_bytes}")
+
+ # Extract the next two bytes for the Function Code
+ function_code_bytes = response[14:18]
+ try:
+ function_code_ascii = bytes.fromhex(function_code_bytes).decode('ascii')
+ function_code_decimal = int(function_code_ascii, 16)
+ print(f"Function Code: {function_code_bytes} (Hex), ASCII: {function_code_ascii}, Decimal: {function_code_decimal}")
+ except ValueError:
+ print(f"Failed to decode function code from bytes: {function_code_bytes}")
+
+ # Extract the next 4 bytes for the Length Code
+ length_code_bytes = response[18:26]
+ try:
+ length_ascii = bytes.fromhex(length_code_bytes).decode('ascii')
+ length_decimal = int(length_ascii, 16)
+ print(f"Length Code: {length_code_bytes} (Hex), ASCII: {length_ascii}, Decimal: {length_decimal}")
+ except ValueError:
+ print(f"Failed to decode length code from bytes: {length_code_bytes}")
+
+ # Extract the next 2 bytes for the Data Flag
+ data_flag_bytes = response[26:30]
+ try:
+ data_flag_ascii = bytes.fromhex(data_flag_bytes).decode('ascii')
+ data_flag_decimal = int(data_flag_ascii, 16)
+ print(f"Data Flag: {data_flag_bytes} (Hex), ASCII: {data_flag_ascii}, Decimal: {data_flag_decimal}")
+ except ValueError:
+ print(f"Failed to decode data flag from bytes: {data_flag_bytes}")
+
+ # Extract the next 2 bytes for the Command Group
+ command_group_bytes = response[30:34]
+ try:
+ command_group_ascii = bytes.fromhex(command_group_bytes).decode('ascii')
+ command_group_decimal = int(command_group_ascii, 16)
+ print(f"Command Group: {command_group_bytes} (Hex), ASCII: {command_group_ascii}, Decimal: {command_group_decimal}")
+ except ValueError:
+ print(f"Failed to decode command group from bytes: {command_group_bytes}")
+
+ # Extract the next 2 bytes for the Number of Cells
+ num_cells_bytes = response[34:38]
+ try:
+ num_cells_ascii = bytes.fromhex(num_cells_bytes).decode('ascii')
+ num_cells_decimal = int(num_cells_ascii, 16)
+ print(f"Number of Cells: {num_cells_bytes} (Hex), ASCII: {num_cells_ascii}, Decimal: {num_cells_decimal}")
+ except ValueError:
+ print(f"Failed to decode number of cells from bytes: {num_cells_bytes}")
+
+ # Extract and process the voltages for all 16 cells
+ for cell_index in range(16):
+ start = 38 + (cell_index * 8)
+ end = start + 8
+ cell_voltage_bytes = response[start:end]
+ try:
+ cell_voltage_ascii = bytes.fromhex(cell_voltage_bytes).decode('ascii')
+ cell_voltage_decimal = int(cell_voltage_ascii, 16) / 1000.0 # Convert to volts
+ print(f"Voltage of Cell {cell_index + 1}: {cell_voltage_bytes} (Hex), ASCII: {cell_voltage_ascii}, Voltage: {cell_voltage_decimal:.3f} V")
+ except ValueError:
+ print(f"Failed to decode Voltage of Cell {cell_index + 1} from bytes: {cell_voltage_bytes}")
+
+ # Extract the number of temperature sensors (4 hex bytes)
+ num_temp_start = 38 + (16 * 8)
+ num_temp_end = num_temp_start + 4
+ num_temp_bytes = response[num_temp_start:num_temp_end]
+ try:
+ num_temp_ascii = bytes.fromhex(num_temp_bytes).decode('ascii')
+ num_temp_decimal = int(num_temp_ascii, 16)
+ print(f"Number of Temperature Sensors: {num_temp_bytes} (Hex), ASCII: {num_temp_ascii}, Decimal: {num_temp_decimal}")
+ except ValueError:
+ print(f"Failed to decode number of temperature sensors from bytes: {num_temp_bytes}")
+
+ # Extract and process additional temperature and battery information
+ current_index = num_temp_end
+
+ # Cell Temperature 1
+ for temp_index in range(1, 5):
+ temp_bytes = response[current_index:current_index + 8]
+ try:
+ temp_ascii = bytes.fromhex(temp_bytes).decode('ascii')
+ temp_decimal = (int(temp_ascii, 16)- 2731 )/ 10.0 # Convert to Celsius
+ print(f"Cell Temperature {temp_index}: {temp_bytes} (Hex), ASCII: {temp_ascii}, Temperature: {temp_decimal:.2f} °C")
+ except ValueError:
+ print(f"Failed to decode Cell Temperature {temp_index} from bytes: {temp_bytes}")
+ current_index += 8
+
+ # Environment Temperature
+ env_temp_bytes = response[current_index:current_index + 8]
+ try:
+ env_temp_ascii = bytes.fromhex(env_temp_bytes).decode('ascii')
+ env_temp_decimal = (int(env_temp_ascii, 16) - 2731 )/ 10.0
+ print(f"Environment Temperature: {env_temp_bytes} (Hex), ASCII: {env_temp_ascii}, Temperature: {env_temp_decimal:.2f} °C")
+ except ValueError:
+ print(f"Failed to decode Environment Temperature from bytes: {env_temp_bytes}")
+ current_index += 8
+
+ # Power Temperature
+ power_temp_bytes = response[current_index:current_index + 8]
+ try:
+ power_temp_ascii = bytes.fromhex(power_temp_bytes).decode('ascii')
+ power_temp_decimal = (int(power_temp_ascii, 16)- 2731 )/ 10.0
+ print(f"Power Temperature: {power_temp_bytes} (Hex), ASCII: {power_temp_ascii}, Temperature: {power_temp_decimal:.2f} °C")
+ except ValueError:
+ print(f"Failed to decode Power Temperature from bytes: {power_temp_bytes}")
+ current_index += 8
+
+ # Charge/Discharge Current
+ current_bytes = response[current_index:current_index + 8]
+ try:
+ current_ascii = bytes.fromhex(current_bytes).decode('ascii')
+ current_decimal = int(current_ascii, 16) / 100.0
+ print(f"Charge/Discharge Current: {current_bytes} (Hex), ASCII: {current_ascii}, Current: {current_decimal:.3f} A")
+ except ValueError:
+ print(f"Failed to decode Charge/Discharge Current from bytes: {current_bytes}")
+ current_index += 8
+
+ # Total Battery Voltage
+ total_voltage_bytes = response[current_index:current_index + 8]
+ try:
+ total_voltage_ascii = bytes.fromhex(total_voltage_bytes).decode('ascii')
+ total_voltage_decimal = int(total_voltage_ascii, 16) / 100.0
+ print(f"Total Battery Voltage: {total_voltage_bytes} (Hex), ASCII: {total_voltage_ascii}, Voltage: {total_voltage_decimal:.3f} V")
+ except ValueError:
+ print(f"Failed to decode Total Battery Voltage from bytes: {total_voltage_bytes}")
+ current_index += 8
+
+ # Residual Capacity
+ residual_capacity_bytes = response[current_index:current_index + 8]
+ try:
+ residual_capacity_ascii = bytes.fromhex(residual_capacity_bytes).decode('ascii')
+ residual_capacity_decimal = int(residual_capacity_ascii, 16) / 100.0
+ print(f"Residual Capacity: {residual_capacity_bytes} (Hex), ASCII: {residual_capacity_ascii}, Capacity: {residual_capacity_decimal:.3f} Ah")
+ except ValueError:
+ print(f"Failed to decode Residual Capacity from bytes: {residual_capacity_bytes}")
+ current_index += 8
+
+ # Custom Number
+ custom_number_bytes = response[current_index:current_index + 4]
+ try:
+ custom_number_ascii = bytes.fromhex(custom_number_bytes).decode('ascii')
+ custom_number_decimal = int(custom_number_ascii, 16)
+ print(f"Custom Number: {custom_number_bytes} (Hex), ASCII: {custom_number_ascii}, Decimal: {custom_number_decimal}")
+ except ValueError:
+ print(f"Failed to decode Custom Number from bytes: {custom_number_bytes}")
+ current_index += 4
+
+ # Battery Capacity
+ battery_capacity_bytes = response[current_index:current_index + 8]
+ try:
+ battery_capacity_ascii = bytes.fromhex(battery_capacity_bytes).decode('ascii')
+ battery_capacity_decimal = int(battery_capacity_ascii, 16) / 100.0
+ print(f"Battery Capacity: {battery_capacity_bytes} (Hex), ASCII: {battery_capacity_ascii}, Capacity: {battery_capacity_decimal:.3f} Ah")
+ except ValueError:
+ print(f"Failed to decode Battery Capacity from bytes: {battery_capacity_bytes}")
+ current_index += 8
+
+ # SOC
+ soc_bytes = response[current_index:current_index + 8]
+ try:
+ soc_ascii = bytes.fromhex(soc_bytes).decode('ascii')
+ soc_decimal = int(soc_ascii, 16) / 10.0
+ print(f"SOC: {soc_bytes} (Hex), ASCII: {soc_ascii}, SOC: {soc_decimal:.2f}%")
+ except ValueError:
+ print(f"Failed to decode SOC from bytes: {soc_bytes}")
+ current_index += 8
+
+ # Rated Capacity
+ rated_capacity_bytes = response[current_index:current_index + 8]
+ try:
+ rated_capacity_ascii = bytes.fromhex(rated_capacity_bytes).decode('ascii')
+ rated_capacity_decimal = int(rated_capacity_ascii, 16) / 100.0
+ print(f"Rated Capacity: {rated_capacity_bytes} (Hex), ASCII: {rated_capacity_ascii}, Capacity: {rated_capacity_decimal:.3f} Ah")
+ except ValueError:
+ print(f"Failed to decode Rated Capacity from bytes: {rated_capacity_bytes}")
+ current_index += 8
+
+ # Number of Cycles
+ num_cycles_bytes = response[current_index:current_index + 8]
+ try:
+ num_cycles_ascii = bytes.fromhex(num_cycles_bytes).decode('ascii')
+ num_cycles_decimal = int(num_cycles_ascii, 16)
+ print(f"Number of Cycles: {num_cycles_bytes} (Hex), ASCII: {num_cycles_ascii}, Cycles: {num_cycles_decimal}")
+ except ValueError:
+ print(f"Failed to decode Number of Cycles from bytes: {num_cycles_bytes}")
+ current_index += 8
+
+ # SOH
+ soh_bytes = response[current_index:current_index + 8]
+ try:
+ soh_ascii = bytes.fromhex(soh_bytes).decode('ascii')
+ soh_decimal = int(soh_ascii, 16) / 10.0
+ print(f"SOH: {soh_bytes} (Hex), ASCII: {soh_ascii}, SOH: {soh_decimal:.2f}%")
+ except ValueError:
+ print(f"Failed to decode SOH from bytes: {soh_bytes}")
+ current_index += 8
+
+ # bus voltage
+ bus_bytes = response[current_index:current_index + 8]
+ try:
+ bus_ascii = bytes.fromhex(bus_bytes).decode('ascii')
+ bus_decimal = int(bus_ascii, 16) / 100.0
+ print(f"bus voltage: {bus_bytes} (Hex), ASCII: {bus_ascii}, bus voltage: {bus_decimal:.2f}V")
+ except ValueError:
+ print(f"Failed to decode bus voltage from bytes: {bus_bytes}")
+
+
+def send_command():
+ # Define the serial port and baud rate
+ port = '/dev/ttyUSB0' # Ensure the full path is correct
+ baudrate = 19200 # Replace with the correct baud rate for your BMS
+
+ # Create the serial connection
+ try:
+ with serial.Serial(port, baudrate, timeout=1) as ser:
+ # Convert the hex string to bytes
+ command = bytes.fromhex("7E3230303134363432453030323031464433350D")
+
+ # Send the command
+ ser.write(command)
+ print("Command sent successfully.")
+
+ # Wait for and read the response
+ response = ser.read(200) # Adjust the number of bytes to read as needed
+ if response:
+ hex_response = response.hex()
+ print("Response received:", hex_response)
+ # Process the response to check details
+ check_starting_byte_and_extract_details(hex_response)
+ else:
+ print("No response received.")
+ except serial.SerialException as e:
+ print(f"Error opening serial port: {e}")
+ except Exception as e:
+ print(f"An unexpected error occurred: {e}")
+
+if __name__ == "__main__":
+ send_command()
diff --git a/csharp/Lib/Devices/BatteryDeligreen/TelecommandFrameParser.cs b/csharp/Lib/Devices/BatteryDeligreen/TelecommandFrameParser.cs
index f50bf70ec..56f32c610 100644
--- a/csharp/Lib/Devices/BatteryDeligreen/TelecommandFrameParser.cs
+++ b/csharp/Lib/Devices/BatteryDeligreen/TelecommandFrameParser.cs
@@ -1,6 +1,145 @@
+using System.Globalization;
+
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
+using System;
+using System.Collections.Generic;
+
public class TelecommandFrameParser
{
+ private static Int32 _currentIndex;
+ private const Int32 FrameLength = 232;
+
+ public Boolean ParsingTelecommandFrame(String response)
+ {
+ _currentIndex = 0; // Reset currentIndex to the start
+
+ if (string.IsNullOrEmpty(response) || response.Length < FrameLength)
+ {
+ Console.WriteLine("Response is too short to contain valid data.");
+ Console.WriteLine(" Fixed Length" + FrameLength);
+ Console.WriteLine(" response Length" + response.Length);
+ return false;
+ }
+
+ // Check starting byte
+ string startingByte = response.Substring(_currentIndex, 2).ToUpper();
+ if (startingByte == "7E")
+ {
+ // Console.WriteLine($"Starting byte: {startingByte} (Hex)");
+ }
+ else
+ {
+ Console.WriteLine($"Incorrect starting byte: {startingByte}");
+ return false;
+ }
+
+ _currentIndex += 2;
+
+ // Extract firmware version
+ var versionBytes = response.Substring(_currentIndex, 4);
+ try
+ {
+ var versionAscii = HexToAscii(versionBytes);
+ // Console.WriteLine($"Firmware version: {versionBytes} (Hex), ASCII: {versionAscii}");
+ }
+ catch (Exception)
+ {
+ Console.WriteLine($"Failed to decode firmware version from bytes: {versionBytes}");
+ return false;
+ }
+
+ _currentIndex += 4;
+
+ // Extract and parse other fields
+ ParseAndPrintHexField(response, "Device Address", 4);
+ ParseAndPrintHexField(response, "Device Code (CID1)", 4);
+ ParseAndPrintHexField(response, "Function Code", 4);
+ ParseAndPrintHexField(response, "Length Code", 8);
+ ParseAndPrintHexField(response, "Data Flag", 4);
+ ParseAndPrintHexField(response, "Command Group", 4);
+ ParseAndPrintHexField(response, "Number of Cells", 4);
+ ExtractCellAlarm(response);
+ return true;
+ }
+
+
-}
\ No newline at end of file
+ private static void ExtractCellAlarm(String response)
+ {
+
+ Dictionary byteAlarmCodes = new Dictionary
+ {
+ { "00", "Normal, no alarm" },
+ { "01", "Alarm that analog quantity reaches the lower limit" },
+ { "02", "Alarm that analog quantity reaches the upper limit" },
+ { "F0", "Other alarms" }
+ };
+
+ // Process Alarms for all 16 cells
+ for (var i = 0; i < 16; i++)
+ {
+ var cellAlarm = response.Substring(_currentIndex, 4);
+ try
+ {
+ var alarmAscii = HexToAscii(cellAlarm);
+ var cellVoltageDecimal = HexToDecimal(alarmAscii);
+ string alarmMessage = byteAlarmCodes.ContainsKey(alarmAscii) ? byteAlarmCodes[alarmAscii] : "Unknown alarm code";
+
+ // Console.WriteLine($"Cell {i + 1}: Alarm Code {cellAlarm}, Status: {alarmMessage}");
+
+ }
+ catch (Exception)
+ {
+ Console.WriteLine($"Failed to decode Voltage of Cell {i + 1} from bytes: {cellAlarm}");
+ }
+ _currentIndex += 4;
+ }
+ }
+
+ private static void ParseAndPrintHexField(String response, String fieldName, int length)
+ {
+ var hexBytes = response.Substring(_currentIndex, length);
+ try
+ {
+ var asciiValue = HexToAscii(hexBytes);
+ var decimalValue = int.Parse(asciiValue, NumberStyles.HexNumber);
+ // Console.WriteLine($"{fieldName}: {hexBytes} (Hex), ASCII: {asciiValue}, Decimal: {decimalValue}");
+ }
+ catch (Exception)
+ {
+ Console.WriteLine($"Failed to decode {fieldName} from bytes: {hexBytes}");
+ }
+ _currentIndex += length;
+ }
+ private static void ParseAndPrintField(String response, String fieldName, Int32 length, Func conversion, String unit)
+ {
+ var fieldBytes = response.Substring(_currentIndex, length);
+ try
+ {
+ var fieldAscii = HexToAscii(fieldBytes);
+ var fieldDecimal = conversion(HexToDecimal(fieldAscii));
+ Console.WriteLine($"{fieldName}: {fieldBytes} (Hex), ASCII: {fieldAscii}, {fieldName}: {fieldDecimal:F3} {unit}");
+ }
+ catch (Exception)
+ {
+ Console.WriteLine($"Failed to decode {fieldName} from bytes: {fieldBytes}");
+ }
+ _currentIndex += length;
+ }
+
+ private static String HexToAscii(String hex)
+ {
+ var bytes = new Byte[hex.Length / 2];
+ for (var i = 0; i < hex.Length; i += 2)
+ {
+ bytes[i / 2] = byte.Parse(hex.Substring(i, 2), NumberStyles.HexNumber);
+ }
+ return System.Text.Encoding.ASCII.GetString(bytes);
+ }
+
+ private static double HexToDecimal(String hex)
+ {
+ return int.Parse(hex, NumberStyles.HexNumber);
+ }
+}
diff --git a/csharp/Lib/Devices/BatteryDeligreen/TelemetryFrameParser.cs b/csharp/Lib/Devices/BatteryDeligreen/TelemetryFrameParser.cs
index 8045c4dc8..bdd2e4218 100644
--- a/csharp/Lib/Devices/BatteryDeligreen/TelemetryFrameParser.cs
+++ b/csharp/Lib/Devices/BatteryDeligreen/TelemetryFrameParser.cs
@@ -1,48 +1,54 @@
using System.Globalization;
+using InnovEnergy.Lib.Units;
+using InnovEnergy.Lib.Utils;
+using static InnovEnergy.Lib.Devices.BatteryDeligreen.BatteryDeligreenDataRecord;
+using static InnovEnergy.Lib.Devices.BatteryDeligreen.Temperatures;
namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
public class TelemetryFrameParser
{
private static Int32 _currentIndex;
- private const Int32 FrameLenght = 286;
+ private const Int32 FrameLenght = 336;
- public void ParsingTelemetryFrame(String response)
+ public BatteryDeligreenDataRecord? ParsingTelemetryFrame(String response)
{
-
_currentIndex = 0; // Reset currentIndex to the start
if (string.IsNullOrEmpty(response) || response.Length < FrameLenght)
{
Console.WriteLine("Response is too short to contain valid data.");
- return;
+ Console.WriteLine("length " + response.Length);
+ return null;
}
// Check starting byte
- string startingByte = response.Substring(_currentIndex, 2).ToUpper();
+ var startingByte = response.Substring(_currentIndex, 2).ToUpper();
if (startingByte == "7E")
{
- Console.WriteLine($"Starting byte: {startingByte} (Hex)");
+ //Console.WriteLine($"Starting byte: {startingByte} (Hex)");
}
else
{
Console.WriteLine($"Incorrect starting byte: {startingByte}");
- return;
+ return null;
}
_currentIndex += 2;
// Extract firmware version
var versionBytes = response.Substring(_currentIndex, 4);
+ var versionAscii = "";
try
{
- String versionAscii = HexToAscii(versionBytes);
- Console.WriteLine($"Firmware version: {versionBytes} (Hex), ASCII: {versionAscii}");
+ versionAscii = HexToAscii(versionBytes);
+ // Console.WriteLine($"Firmware version: {versionBytes} (Hex), ASCII: {versionAscii}");
}
catch (Exception)
{
Console.WriteLine($"Failed to decode firmware version from bytes: {versionBytes}");
- return;
+ return null;
}
+
_currentIndex += 4;
// Extract and parse other fields
@@ -54,15 +60,54 @@ public class TelemetryFrameParser
ParseAndPrintHexField(response, "Command Group", 4);
ParseAndPrintHexField(response, "Number of Cells", 4);
+ var cellVoltages = ExtractCellVoltage(response);
+
+ // Parse other fields
+ ParseAndPrintHexField(response, "Number of Temperature Sensors", 4);
+ var cellTemperature = new List();
+
+ // Parse cell temperatures
+ for (var i = 1; i <= 4; i++)
+ {
+ cellTemperature.Add(ParseAndPrintTemperatureField(response, $"Cell Temperature {i}"));
+ }
+
+ // Parse other temperature and battery information
+ var environmentTemp = ParseAndPrintTemperatureField(response, "Environment Temperature");
+ var powerTemp = ParseAndPrintTemperatureField(response, "Power Temperature");
+ var current = ParseAndPrintField(response, "Charge/Discharge Current" , 8, value => value / 100.0, "A");
+ var totalBatteryVoltage = ParseAndPrintField(response, "Total Battery Voltage" , 8, value => value / 100.0, "V");
+ var residualCapacity = ParseAndPrintField(response, "Residual Capacity" , 8, value => value / 100.0, "Ah");
+ var customNumber = ParseAndPrintHexField(response, "Custom Number" , 4);
+ var batteryCapacity = ParseAndPrintField(response, "Battery Capacity" , 8, value => value / 100.0, "Ah");
+ var soc = ParseAndPrintField(response, "SOC" , 8, value => value / 10.0, "%");
+ var ratedCapacity = ParseAndPrintField(response, "Rated Capacity" , 8, value => value / 100.0, "Ah");
+ var numberOfCycle = ParseAndPrintHexField(response, "Number of Cycles" , 8);
+ var soh = ParseAndPrintField(response, "SOH" , 8, value => value / 10.0, "%");
+ var busVoltage = ParseAndPrintField(response, "Bus Voltage" , 8, value => value / 100.0, "V");
+
+ var temperatures = new TemperaturesList(cellTemperature[0], cellTemperature[1], cellTemperature[2],
+ cellTemperature[3], environmentTemp, powerTemp);
+
+ var batteryRecord = new BatteryDeligreenDataRecord(busVoltage, current, versionAscii, soc, numberOfCycle, batteryCapacity, ratedCapacity,
+ totalBatteryVoltage, soh, residualCapacity, cellVoltages, temperatures);
+
+ return batteryRecord;
+ }
+
+ private static List ExtractCellVoltage(String response)
+ {
+ var cellVoltages = new List();
// Process voltages for all 16 cells
for (var i = 0; i < 16; i++)
{
- String cellVoltageBytes = response.Substring(_currentIndex, 8);
+ var cellVoltageBytes = response.Substring(_currentIndex, 8);
try
{
var cellVoltageAscii = HexToAscii(cellVoltageBytes);
var cellVoltageDecimal = HexToDecimal(cellVoltageAscii) / 1000.0; // cell voltage are divided 1000
- Console.WriteLine($"Voltage of Cell {i + 1}: {cellVoltageBytes} (Hex), ASCII: {cellVoltageAscii}, Voltage: {cellVoltageDecimal:F3} V");
+ cellVoltages.Add(cellVoltageDecimal);
+ // Console.WriteLine($"Voltage of Cell {i + 1}: {cellVoltageBytes} (Hex), ASCII: {cellVoltageAscii}, Voltage: {cellVoltageDecimal:F3} V");
}
catch (Exception)
{
@@ -70,77 +115,77 @@ public class TelemetryFrameParser
}
_currentIndex += 8;
}
-
- // Parse other fields
- ParseAndPrintHexField(response, "Number of Temperature Sensors", 4);
-
- // Parse cell temperatures
- for (var i = 1; i <= 4; i++)
- {
- ParseAndPrintTemperatureField(response, $"Cell Temperature {i}");
- }
-
- // Parse other temperature and battery information
- ParseAndPrintTemperatureField(response, "Environment Temperature");
- ParseAndPrintTemperatureField(response, "Power Temperature");
- ParseAndPrintField(response, "Charge/Discharge Current", 8, value => value / 100.0, "A");
- ParseAndPrintField(response, "Total Battery Voltage", 8, value => value / 100.0, "V");
- ParseAndPrintField(response, "Residual Capacity", 8, value => value / 100.0, "Ah");
- ParseAndPrintHexField(response, "Custom Number", 4);
- ParseAndPrintField(response, "Battery Capacity", 8, value => value / 100.0, "Ah");
- ParseAndPrintField(response, "SOC", 8, value => value / 10.0, "%");
- ParseAndPrintField(response, "Rated Capacity", 8, value => value / 100.0, "Ah");
- ParseAndPrintHexField(response, "Number of Cycles", 8);
- ParseAndPrintField(response, "SOH", 8, value => value / 10.0, "%");
- ParseAndPrintField(response, "Bus Voltage", 8, value => value / 100.0, "V");
+ return cellVoltages;
}
- private static void ParseAndPrintHexField(String response, String fieldName, int length)
+ private static UInt16 ParseAndPrintHexField(String response, String fieldName, Int32 length)
{
var hexBytes = response.Substring(_currentIndex, length);
+ var decimalValue = 0;
try
{
var asciiValue = HexToAscii(hexBytes);
- var decimalValue = int.Parse(asciiValue, NumberStyles.HexNumber);
- Console.WriteLine($"{fieldName}: {hexBytes} (Hex), ASCII: {asciiValue}, Decimal: {decimalValue}");
+ decimalValue = int.Parse(asciiValue, NumberStyles.HexNumber);
+ // Console.WriteLine($"{fieldName}: {hexBytes} (Hex), ASCII: {asciiValue}, Decimal: {decimalValue}");
}
catch (Exception)
{
Console.WriteLine($"Failed to decode {fieldName} from bytes: {hexBytes}");
}
_currentIndex += length;
+ return (UInt16)decimalValue;
+
}
- private static void ParseAndPrintTemperatureField(String response, String fieldName)
+ private static Double ParseAndPrintTemperatureField(String response, String fieldName)
{
var tempBytes = response.Substring(_currentIndex, 8);
+ var tempDecimal = 0.0;
try
{
var tempAscii = HexToAscii(tempBytes);
- var tempDecimal = (HexToDecimal(tempAscii) - 2731) / 10.0;
- Console.WriteLine($"{fieldName}: {tempBytes} (Hex), ASCII: {tempAscii}, Temperature: {tempDecimal:F2} °C");
+ tempDecimal = (HexToDecimal(tempAscii) - 2731) / 10.0;
+ // Console.WriteLine($"{fieldName}: {tempBytes} (Hex), ASCII: {tempAscii}, Temperature: {tempDecimal:F2} °C");
}
catch (Exception)
{
Console.WriteLine($"Failed to decode {fieldName} from bytes: {tempBytes}");
}
_currentIndex += 8;
+ return tempDecimal;
}
- private static void ParseAndPrintField(String response, String fieldName, Int32 length, Func conversion, String unit)
+ private static Double ParseAndPrintField(String response, String fieldName, Int32 length, Func conversion, String unit)
{
var fieldBytes = response.Substring(_currentIndex, length);
+ var value = 0.0;
try
{
var fieldAscii = HexToAscii(fieldBytes);
- var fieldDecimal = conversion(HexToDecimal(fieldAscii));
- Console.WriteLine($"{fieldName}: {fieldBytes} (Hex), ASCII: {fieldAscii}, {fieldName}: {fieldDecimal:F3} {unit}");
+ var fieldDouble = 0.0;
+
+
+ // Convert from Hex to Integer using Two's Complement logic
+ Int32 intValue = Convert.ToInt16(fieldAscii, 16);
+ var bitLength = (length/2) * 4; // Each hex digit is 4 bits
+ var maxPositiveValue = 1 << (bitLength - 1); // 2^(bitLength-1)
+
+ if (intValue >= maxPositiveValue)
+ {
+ intValue -= (1 << bitLength); // Apply two's complement conversion
+ }
+
+ fieldDouble = conversion(intValue); // Store the converted negative value as string
+
+ //Console.WriteLine($"{fieldName}: {fieldBytes} (Hex), ASCII: {fieldAscii}, {fieldName}: {fieldDouble:F3} {unit}");
+ value = fieldDouble;
}
catch (Exception)
{
Console.WriteLine($"Failed to decode {fieldName} from bytes: {fieldBytes}");
}
_currentIndex += length;
+ return value;
}
private static String HexToAscii(String hex)
@@ -153,7 +198,7 @@ public class TelemetryFrameParser
return System.Text.Encoding.ASCII.GetString(bytes);
}
- private static double HexToDecimal(String hex)
+ private static Double HexToDecimal(String hex)
{
return int.Parse(hex, NumberStyles.HexNumber);
}
diff --git a/csharp/Lib/Devices/BatteryDeligreen/Temperatures.cs b/csharp/Lib/Devices/BatteryDeligreen/Temperatures.cs
new file mode 100644
index 000000000..257f14aca
--- /dev/null
+++ b/csharp/Lib/Devices/BatteryDeligreen/Temperatures.cs
@@ -0,0 +1,26 @@
+using InnovEnergy.Lib.Units;
+
+namespace InnovEnergy.Lib.Devices.BatteryDeligreen;
+
+public class Temperatures
+{
+ public struct TemperaturesList
+ {
+ public Temperature CellTemperature1 {get;}
+ public Temperature CellTemperature2 {get;}
+ public Temperature CellTemperature3 {get;}
+ public Temperature CellTemperature4 {get;}
+ public Temperature EnvironmentTemperature {get;}
+ public Temperature PowerTemperature {get;}
+
+ public TemperaturesList(Temperature cell1, Temperature cell2, Temperature cell3, Temperature cell4, Temperature environment, Temperature power)
+ {
+ CellTemperature1 = cell1;
+ CellTemperature2 = cell2;
+ CellTemperature3 = cell3;
+ CellTemperature4 = cell4;
+ EnvironmentTemperature = environment;
+ PowerTemperature = power;
+ }
+ }
+}
\ No newline at end of file