Merge pull request #541 from M4GNV5/feat/pylon-lv

Add new inverter implementation "Pylontech LV"
This commit is contained in:
Daniel Öster 2024-11-04 11:42:16 +02:00 committed by GitHub
commit 293ee65f65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 173 additions and 3 deletions

View file

@ -39,7 +39,8 @@
//#define BYD_SMA //Enable this line to emulate a SMA compatible "BYD Battery-Box HVS 10.2KW battery" over CAN bus //#define BYD_SMA //Enable this line to emulate a SMA compatible "BYD Battery-Box HVS 10.2KW battery" over CAN bus
//#define BYD_MODBUS //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU //#define BYD_MODBUS //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU
//#define FOXESS_CAN //Enable this line to emulate a "HV2600/ECS4100 battery" over CAN bus //#define FOXESS_CAN //Enable this line to emulate a "HV2600/ECS4100 battery" over CAN bus
//#define PYLON_CAN //Enable this line to emulate a "Pylontech battery" over CAN bus //#define PYLON_LV_CAN //Enable this line to emulate a "48V Pylontech battery" over CAN bus
//#define PYLON_CAN //Enable this line to emulate a "High Voltage Pylontech battery" over CAN bus
//#define SMA_CAN //Enable this line to emulate a "BYD Battery-Box H 8.9kWh, 7 mod" over CAN bus //#define SMA_CAN //Enable this line to emulate a "BYD Battery-Box H 8.9kWh, 7 mod" over CAN bus
//#define SMA_TRIPOWER_CAN //Enable this line to emulate a "SMA Home Storage battery" over CAN bus //#define SMA_TRIPOWER_CAN //Enable this line to emulate a "SMA Home Storage battery" over CAN bus
//#define SOFAR_CAN //Enable this line to emulate a "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame)" over CAN bus //#define SOFAR_CAN //Enable this line to emulate a "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame)" over CAN bus
@ -108,6 +109,10 @@
#define BATTERY_MAXPERCENTAGE 8000 #define BATTERY_MAXPERCENTAGE 8000
// 2000 = 20.0% , Min percentage the battery will discharge to (Inverter gets 0% when reached) // 2000 = 20.0% , Min percentage the battery will discharge to (Inverter gets 0% when reached)
#define BATTERY_MINPERCENTAGE 2000 #define BATTERY_MINPERCENTAGE 2000
// 500 = 50.0 °C , Max temperature (Will produce a battery overheat event if above)
#define BATTERY_MAXTEMPERATURE 500
// -250 = -25.0 °C , Min temperature (Will produce a battery frozen event if below)
#define BATTERY_MINTEMPERATURE -250
// 300 = 30.0A , BYD CAN specific setting, Max charge in Amp (Some inverters needs to be limited) // 300 = 30.0A , BYD CAN specific setting, Max charge in Amp (Some inverters needs to be limited)
#define BATTERY_MAX_CHARGE_AMP 300 #define BATTERY_MAX_CHARGE_AMP 300
// 300 = 30.0A , BYD CAN specific setting, Max discharge in Amp (Some inverters needs to be limited) // 300 = 30.0A , BYD CAN specific setting, Max discharge in Amp (Some inverters needs to be limited)

View file

@ -27,14 +27,14 @@ void update_machineryprotection() {
} }
// Battery is overheated! // Battery is overheated!
if (datalayer.battery.status.temperature_max_dC > 500) { if (datalayer.battery.status.temperature_max_dC > BATTERY_MAXTEMPERATURE) {
set_event(EVENT_BATTERY_OVERHEAT, datalayer.battery.status.temperature_max_dC); set_event(EVENT_BATTERY_OVERHEAT, datalayer.battery.status.temperature_max_dC);
} else { } else {
clear_event(EVENT_BATTERY_OVERHEAT); clear_event(EVENT_BATTERY_OVERHEAT);
} }
// Battery is frozen! // Battery is frozen!
if (datalayer.battery.status.temperature_min_dC < -250) { if (datalayer.battery.status.temperature_min_dC < BATTERY_MINTEMPERATURE) {
set_event(EVENT_BATTERY_FROZEN, datalayer.battery.status.temperature_min_dC); set_event(EVENT_BATTERY_FROZEN, datalayer.battery.status.temperature_min_dC);
} else { } else {
clear_event(EVENT_BATTERY_FROZEN); clear_event(EVENT_BATTERY_FROZEN);

View file

@ -495,6 +495,9 @@ String processor(const String& var) {
#ifdef PYLON_CAN #ifdef PYLON_CAN
content += "Pylontech battery over CAN bus"; content += "Pylontech battery over CAN bus";
#endif // PYLON_CAN #endif // PYLON_CAN
#ifdef PYLON_LV_CAN
content += "Pylontech LV battery over CAN bus";
#endif // PYLON_LV_CAN
#ifdef SERIAL_LINK_TRANSMITTER #ifdef SERIAL_LINK_TRANSMITTER
content += "Serial link to another LilyGo board"; content += "Serial link to another LilyGo board";
#endif // SERIAL_LINK_TRANSMITTER #endif // SERIAL_LINK_TRANSMITTER

View file

@ -27,6 +27,10 @@
#include "PYLON-CAN.h" #include "PYLON-CAN.h"
#endif #endif
#ifdef PYLON_LV_CAN
#include "PYLON-LV-CAN.h"
#endif
#ifdef SMA_CAN #ifdef SMA_CAN
#include "SMA-CAN.h" #include "SMA-CAN.h"
#endif #endif

View file

@ -0,0 +1,142 @@
#include "../include.h"
#ifdef PYLON_LV_CAN
#include "../datalayer/datalayer.h"
#include "PYLON-LV-CAN.h"
/* Do not change code below unless you are sure what you are doing */
static unsigned long previousMillis1000ms = 0;
CAN_frame PYLON_351 = {.FD = false,
.ext_ID = false,
.DLC = 6,
.ID = 0x351,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame PYLON_355 = {.FD = false, .ext_ID = false, .DLC = 4, .ID = 0x355, .data = {0x00, 0x00, 0x00, 0x00}};
CAN_frame PYLON_356 = {.FD = false,
.ext_ID = false,
.DLC = 6,
.ID = 0x356,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame PYLON_359 = {.FD = false,
.ext_ID = false,
.DLC = 7,
.ID = 0x359,
.data = {0x00, 0x00, 0x00, 0x00, PACK_NUMBER, 'P', 'N'}};
CAN_frame PYLON_35C = {.FD = false, .ext_ID = false, .DLC = 2, .ID = 0x35C, .data = {0x00, 0x00}};
CAN_frame PYLON_35E = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x35E,
.data = {
MANUFACTURER_NAME[0],
MANUFACTURER_NAME[1],
MANUFACTURER_NAME[2],
MANUFACTURER_NAME[3],
MANUFACTURER_NAME[4],
MANUFACTURER_NAME[5],
MANUFACTURER_NAME[6],
MANUFACTURER_NAME[7],
}};
void update_values_can_inverter() {
// This function maps all the values fetched from battery CAN to the correct CAN messages
// do not update values unless we have some voltage, as we will run into IntegerDivideByZero exceptions otherwise
if (datalayer.battery.status.voltage_dV == 0)
return;
// TODO: officially this value is "battery charge voltage". Do we need to add something here to the actual voltage?
PYLON_351.data.u8[0] = datalayer.battery.status.voltage_dV & 0xff;
PYLON_351.data.u8[1] = datalayer.battery.status.voltage_dV >> 8;
int16_t maxChargeCurrent = datalayer.battery.status.max_charge_power_W * 100 / datalayer.battery.status.voltage_dV;
PYLON_351.data.u8[2] = maxChargeCurrent & 0xff;
PYLON_351.data.u8[3] = maxChargeCurrent >> 8;
int16_t maxDischargeCurrent =
datalayer.battery.status.max_discharge_power_W * 100 / datalayer.battery.status.voltage_dV;
PYLON_351.data.u8[4] = maxDischargeCurrent & 0xff;
PYLON_351.data.u8[5] = maxDischargeCurrent >> 8;
PYLON_355.data.u8[0] = (datalayer.battery.status.reported_soc / 10) & 0xff;
PYLON_355.data.u8[1] = (datalayer.battery.status.reported_soc / 10) >> 8;
PYLON_355.data.u8[2] = (datalayer.battery.status.soh_pptt / 10) & 0xff;
PYLON_355.data.u8[3] = (datalayer.battery.status.soh_pptt / 10) >> 8;
PYLON_356.data.u8[0] = datalayer.battery.status.voltage_dV & 0xff;
PYLON_356.data.u8[1] = datalayer.battery.status.voltage_dV >> 8;
PYLON_356.data.u8[2] = datalayer.battery.status.current_dA & 0xff;
PYLON_356.data.u8[3] = datalayer.battery.status.current_dA >> 8;
PYLON_356.data.u8[4] = datalayer.battery.status.temperature_max_dC & 0xff;
PYLON_356.data.u8[5] = datalayer.battery.status.temperature_max_dC >> 8;
// initialize all errors and warnings to 0
PYLON_359.data.u8[0] = 0x00;
PYLON_359.data.u8[1] = 0x00;
PYLON_359.data.u8[2] = 0x00;
PYLON_359.data.u8[3] = 0x00;
PYLON_359.data.u8[4] = PACK_NUMBER;
PYLON_359.data.u8[5] = 'P';
PYLON_359.data.u8[6] = 'N';
// ERRORS
if (datalayer.battery.status.current_dA >= maxDischargeCurrent)
PYLON_359.data.u8[0] |= 0x80;
if (datalayer.battery.status.temperature_min_dC <= BATTERY_MINTEMPERATURE)
PYLON_359.data.u8[0] |= 0x10;
if (datalayer.battery.status.temperature_max_dC >= BATTERY_MAXTEMPERATURE)
PYLON_359.data.u8[0] |= 0x0C;
if (datalayer.battery.status.voltage_dV * 100 <= datalayer.battery.info.min_cell_voltage_mV)
PYLON_359.data.u8[0] |= 0x04;
// we never set PYLON_359.data.u8[1] |= 0x80 called "BMS internal"
if (datalayer.battery.status.current_dA <= -1 * maxChargeCurrent)
PYLON_359.data.u8[1] |= 0x01;
// WARNINGS (using same rules as errors but reporting earlier)
if (datalayer.battery.status.current_dA >= maxDischargeCurrent * WARNINGS_PERCENT / 100)
PYLON_359.data.u8[2] |= 0x80;
if (datalayer.battery.status.temperature_min_dC <= BATTERY_MINTEMPERATURE * WARNINGS_PERCENT / 100)
PYLON_359.data.u8[2] |= 0x10;
if (datalayer.battery.status.temperature_max_dC >= BATTERY_MAXTEMPERATURE * WARNINGS_PERCENT / 100)
PYLON_359.data.u8[2] |= 0x0C;
if (datalayer.battery.status.voltage_dV * 100 <= datalayer.battery.info.min_cell_voltage_mV + 100)
PYLON_359.data.u8[2] |= 0x04;
// we never set PYLON_359.data.u8[3] |= 0x80 called "BMS internal"
if (datalayer.battery.status.current_dA <= -1 * maxChargeCurrent * WARNINGS_PERCENT / 100)
PYLON_359.data.u8[3] |= 0x01;
PYLON_35C.data.u8[0] = 0xC0; // enable charging and discharging
PYLON_35C.data.u8[1] = 0x00;
if (datalayer.battery.status.real_soc <= datalayer.battery.settings.min_percentage)
PYLON_35C.data.u8[0] = 0xA0; // enable charing, set charge immediately
if (datalayer.battery.status.real_soc >= datalayer.battery.settings.max_percentage)
PYLON_35C.data.u8[0] = 0x40; // enable discharging only
// PYLON_35E is pre-filled with the manufacturer name
}
void receive_can_inverter(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x305: //Message originating from inverter.
// according to the spec, this message includes only 0-bytes
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
break;
default:
break;
}
}
void send_can_inverter() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis1000ms >= 1000) {
previousMillis1000ms = currentMillis;
transmit_can(&PYLON_351, can_config.inverter);
transmit_can(&PYLON_355, can_config.inverter);
transmit_can(&PYLON_356, can_config.inverter);
transmit_can(&PYLON_359, can_config.inverter);
transmit_can(&PYLON_35C, can_config.inverter);
transmit_can(&PYLON_35E, can_config.inverter);
}
}
#endif

View file

@ -0,0 +1,16 @@
#ifndef PYLON_LV_CAN_H
#define PYLON_LV_CAN_H
#include "../include.h"
#define CAN_INVERTER_SELECTED
#define MANUFACTURER_NAME "BatEmuLV"
#define PACK_NUMBER 0x01
// 80 means after reaching 80% of a nominal value a warning is produced (e.g. 80% of max current)
#define WARNINGS_PERCENT 80
void send_system_data();
void send_setup_info();
void transmit_can(CAN_frame* tx_frame, int interface);
#endif