Skip to content

Commit 98d1693

Browse files
committed
added PYLON2_RS485 inverter binding
1 parent 49ec96c commit 98d1693

11 files changed

Lines changed: 271 additions & 0 deletions

File tree

bms-to-inverter-main/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,20 @@
395395
<version>${project.version}</version>
396396
</dependency>
397397

398+
<!-- #################### PYLON (RS485) ################### -->
399+
<dependency>
400+
<groupId>com.ai-republic.bms-to-inverter</groupId>
401+
<artifactId>inverter-pylon-rs485</artifactId>
402+
<version>${project.version}</version>
403+
</dependency>
404+
405+
<!-- #################### PYLON2 (RS485) ################### -->
406+
<dependency>
407+
<groupId>com.ai-republic.bms-to-inverter</groupId>
408+
<artifactId>inverter-pylon2-rs485</artifactId>
409+
<version>${project.version}</version>
410+
</dependency>
411+
398412
<!-- #################### SMA Sunny Island (CAN) ################### -->
399413
<dependency>
400414
<groupId>com.ai-republic.bms-to-inverter</groupId>
4.83 KB
Binary file not shown.

configurator/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,13 @@
371371
<artifactId>inverter-pylon-rs485</artifactId>
372372
<version>${project.version}</version>
373373
</dependency>
374+
375+
<!-- #################### PYLON2 (RS485) ################### -->
376+
<dependency>
377+
<groupId>com.ai-republic.bms-to-inverter</groupId>
378+
<artifactId>inverter-pylon2-rs485</artifactId>
379+
<version>${project.version}</version>
380+
</dependency>
374381

375382
<!-- #################### SMA Sunny Island (CAN) ################### -->
376383
<dependency>

configurator/src/main/resources/META-INF/services/com.airepublic.bmstoinverter.core.InverterDescriptor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ com.airepublic.bmstoinverter.inverter.luxpower.can.LuxpowerInverterCANDescriptor
1010
com.airepublic.bmstoinverter.inverter.pylon.can.PylonInverterCANDescriptor
1111
com.airepublic.bmstoinverter.inverter.pylonhv.can.PylonHVInverterCANDescriptor
1212
com.airepublic.bmstoinverter.inverter.pylon.rs485.PylonInverterRS485Descriptor
13+
com.airepublic.bmstoinverter.inverter.pylon.rs485.Pylon2InverterRS485Descriptor
1314
com.airepublic.bmstoinverter.inverter.sma.can.SMAInverterCANDescriptor
1415
com.airepublic.bmstoinverter.inverter.solark.can.SolArkInverterCANDescriptor
1516
com.airepublic.bmstoinverter.inverter.solishv.can.SolisHVInverterCANDescriptor

inverter-pylon2-rs485/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/target/
2+
/.settings/
3+
/.classpath
4+
/.project

inverter-pylon2-rs485/pom.xml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>com.ai-republic.bms-to-inverter</groupId>
8+
<artifactId>bms-to-inverter-parent</artifactId>
9+
<version>0.0.1-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>inverter-pylon2-rs485</artifactId>
13+
14+
<name>${project.artifactId}-${project.version}</name>
15+
<description>Module for the Pylontech inverter RS485 support</description>
16+
17+
<properties>
18+
<encoding>UTF-8</encoding>
19+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
20+
</properties>
21+
22+
<dependencies>
23+
<dependency>
24+
<groupId>com.ai-republic.bms-to-inverter</groupId>
25+
<artifactId>protocol-rs485</artifactId>
26+
<version>${project.version}</version>
27+
</dependency>
28+
</dependencies>
29+
</project>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* This software is free to use and to distribute in its unchanged form for private use.
3+
* Commercial use is prohibited without an explicit license agreement of the copyright holder.
4+
* Any changes to this software must be made solely in the project repository at https://github.com/ai-republic/bms-to-inverter.
5+
* The copyright holder is not liable for any damages in whatever form that may occur by using this software.
6+
*
7+
* (c) Copyright 2022 and onwards - Torsten Oltmanns
8+
*
9+
* @author Torsten Oltmanns - bms-to-inverter''AT''gmail.com
10+
*/
11+
package com.airepublic.bmstoinverter.inverter.pylon.rs485;
12+
13+
import com.airepublic.bmstoinverter.core.Inverter;
14+
import com.airepublic.bmstoinverter.core.InverterConfig;
15+
import com.airepublic.bmstoinverter.core.InverterDescriptor;
16+
import com.airepublic.bmstoinverter.core.Port;
17+
import com.airepublic.bmstoinverter.core.protocol.rs485.FrameDefinition;
18+
import com.airepublic.bmstoinverter.protocol.rs485.JSerialCommPort;
19+
import com.fazecast.jSerialComm.SerialPort;
20+
21+
/**
22+
* The {@link InverterDescriptor} for the Pylontech {@link Inverter} using the RS485 protocol.
23+
*/
24+
public class Pylon2InverterRS485Descriptor implements InverterDescriptor {
25+
@Override
26+
public String getName() {
27+
return "PYLON2_RS485";
28+
}
29+
30+
31+
@Override
32+
public int getDefaultBaudRate() {
33+
return 9600;
34+
}
35+
36+
37+
@Override
38+
public Class<? extends Inverter> getInverterClass() {
39+
return Pylon2InverterRS485Processor.class;
40+
}
41+
42+
43+
@Override
44+
public Port createPort(final InverterConfig config) {
45+
final Port port = new JSerialCommPort(config.getPortLocator(), config.getBaudRate(), 8, 1, SerialPort.NO_PARITY, new byte[] { (byte) 0x7E, (byte) 0x32 }, FrameDefinition.create("SOAAOCCCCOLLLDVVVVO"));
46+
return port;
47+
}
48+
49+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* This software is free to use and to distribute in its unchanged form for private use.
3+
* Commercial use is prohibited without an explicit license agreement of the copyright holder.
4+
* Any changes to this software must be made solely in the project repository at https://github.com/ai-republic/bms-to-inverter.
5+
* The copyright holder is not liable for any damages in whatever form that may occur by using this software.
6+
*
7+
* (c) Copyright 2022 and onwards - Torsten Oltmanns
8+
*
9+
* @author Torsten Oltmanns - bms-to-inverter''AT''gmail.com
10+
*/
11+
package com.airepublic.bmstoinverter.inverter.pylon.rs485;
12+
13+
import java.io.IOException;
14+
import java.nio.ByteBuffer;
15+
import java.nio.ByteOrder;
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
19+
import javax.enterprise.context.ApplicationScoped;
20+
import javax.inject.Inject;
21+
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
import com.airepublic.bmstoinverter.core.Inverter;
26+
import com.airepublic.bmstoinverter.core.Port;
27+
import com.airepublic.bmstoinverter.core.bms.data.BatteryPack;
28+
import com.airepublic.bmstoinverter.core.bms.data.EnergyStorage;
29+
import com.airepublic.bmstoinverter.core.bms.data.EnergyStorageQualifier;
30+
31+
@ApplicationScoped
32+
public class Pylon2InverterRS485Processor extends Inverter {
33+
private final static Logger LOG = LoggerFactory.getLogger(Pylon2InverterRS485Processor.class);
34+
35+
@Inject
36+
@EnergyStorageQualifier
37+
private EnergyStorage energyStorage;
38+
39+
public Pylon2InverterRS485Processor() {
40+
super();
41+
}
42+
43+
44+
protected Pylon2InverterRS485Processor(final EnergyStorage energyStorage) {
45+
super(energyStorage);
46+
this.energyStorage = energyStorage;
47+
}
48+
49+
50+
@Override
51+
protected List<ByteBuffer> createSendFrames(final ByteBuffer requestFrame, final BatteryPack aggregatedPack) {
52+
final List<ByteBuffer> frames = new ArrayList<>();
53+
54+
double sumSOC = 0;
55+
double sumVoltage = 0;
56+
int count = 0;
57+
58+
// Settings for charging current (Amperes)
59+
final int targetLimitAmps = 100;
60+
final int balanceLimitAmps = 32; // Inverter will throttle this to ~8A actual charge at high SOC
61+
62+
if (energyStorage != null && !energyStorage.getBatteryPacks().isEmpty()) {
63+
for (final BatteryPack pack : energyStorage.getBatteryPacks()) {
64+
if (pack != null && pack.packVoltage > 100) {
65+
final double currentSoc = pack.packSOC > 100 ? pack.packSOC / 10.0 : pack.packSOC;
66+
sumSOC += currentSoc;
67+
sumVoltage += pack.packVoltage;
68+
count++;
69+
}
70+
}
71+
}
72+
73+
if (count == 0) {
74+
return frames;
75+
}
76+
77+
final int avgSoc = (int) Math.round(sumSOC / count);
78+
final double avgVolt = sumVoltage / count;
79+
80+
// Adaptive limit: 100A below 98% SOC, 32A at or above 98% SOC
81+
final int currentAmps = avgSoc >= 98 ? balanceLimitAmps : targetLimitAmps;
82+
83+
// SCALE 100 (Based on ESP32 logic: 100A * 100 = 10000 / 32A * 100 = 3200)
84+
final short rawLimit = (short) (currentAmps * 100);
85+
86+
final ByteBuffer buffer = ByteBuffer.allocate(43).order(ByteOrder.BIG_ENDIAN);
87+
buffer.put((byte) 0x01); // Slave ID
88+
buffer.put((byte) 0x03); // Function
89+
buffer.put((byte) 0x26); // Data Length 38
90+
91+
// [Offset 3-4] Voltage (V * 10)
92+
buffer.putShort((short) Math.round(avgVolt));
93+
94+
// [Offset 5-13] Padding (9 bytes)
95+
for (int i = 0; i < 9; i++) {
96+
buffer.put((byte) 0x00);
97+
}
98+
99+
// [Offset 14] SOC (1 byte)
100+
buffer.put((byte) avgSoc);
101+
102+
// [Offset 15-20] Padding (6 bytes)
103+
for (int i = 0; i < 6; i++) {
104+
buffer.put((byte) 0x00);
105+
}
106+
107+
// [Offset 21-22] Charge Current Limit (Scale 100)
108+
buffer.putShort(rawLimit);
109+
110+
// [Offset 23-24] Discharge Current Limit (Scale 100)
111+
buffer.putShort((short) (targetLimitAmps * 100));
112+
113+
// Padding until CRC
114+
while (buffer.position() < 41) {
115+
buffer.put((byte) 0x00);
116+
}
117+
118+
final byte[] data = buffer.array();
119+
final int crc = calculateCRC(data, 41);
120+
buffer.put((byte) (crc & 0xFF));
121+
buffer.put((byte) (crc >> 8 & 0xFF));
122+
123+
buffer.rewind();
124+
frames.add(buffer);
125+
126+
LOG.info("Bridge -> Inverter: SOC={}%, TargetLimit={}A, SentRaw={}, Count={}",
127+
avgSoc, currentAmps, rawLimit, count);
128+
129+
return frames;
130+
}
131+
132+
133+
@Override
134+
protected ByteBuffer readRequest(final Port port) throws IOException {
135+
return port.receiveFrame();
136+
}
137+
138+
139+
@Override
140+
protected void sendFrame(final Port port, final ByteBuffer frame) throws IOException {
141+
port.sendFrame(frame);
142+
}
143+
144+
145+
private int calculateCRC(final byte[] buf, final int len) {
146+
int crc = 0xFFFF;
147+
for (int pos = 0; pos < len; pos++) {
148+
crc ^= buf[pos] & 0xFF;
149+
for (int i = 8; i != 0; i--) {
150+
if ((crc & 0x0001) != 0) {
151+
crc >>= 1;
152+
crc ^= 0xA001;
153+
} else {
154+
crc >>= 1;
155+
}
156+
}
157+
}
158+
return crc;
159+
}
160+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
3+
bean-discovery-mode="annotated" version="4.0">
4+
5+
</beans>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.airepublic.bmstoinverter.inverter.pylon.rs485.Pylon2InverterRS485Descriptor

0 commit comments

Comments
 (0)