Merge branch 'main' into feature/LEAF-30-reset-SOH

This commit is contained in:
Daniel Öster 2024-11-08 12:53:32 +02:00
commit f6861a6b78
19 changed files with 994 additions and 313 deletions

View file

@ -53,7 +53,7 @@
Preferences settings; // Store user settings Preferences settings; // Store user settings
// The current software version, shown on webserver // The current software version, shown on webserver
const char* version_number = "7.6.dev"; const char* version_number = "7.7.dev";
// Interval settings // Interval settings
uint16_t intervalUpdateValues = INTERVAL_1_S; // Interval at which to update inverter values / Modbus registers uint16_t intervalUpdateValues = INTERVAL_1_S; // Interval at which to update inverter values / Modbus registers
@ -85,6 +85,9 @@ uint16_t mbPV[MB_RTU_NUM_VALUES]; // Process variable memory
// Create a ModbusRTU server instance listening on Serial2 with 2000ms timeout // Create a ModbusRTU server instance listening on Serial2 with 2000ms timeout
ModbusServerRTU MBserver(Serial2, 2000); ModbusServerRTU MBserver(Serial2, 2000);
#endif #endif
#if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER)
#define SERIAL_LINK_BAUDRATE 112500
#endif
// Common charger parameters // Common charger parameters
volatile float charger_setpoint_HV_VDC = 0.0f; volatile float charger_setpoint_HV_VDC = 0.0f;
@ -741,6 +744,7 @@ void handle_contactors() {
if (timeSpentInFaultedMode > MAX_ALLOWED_FAULT_TICKS || if (timeSpentInFaultedMode > MAX_ALLOWED_FAULT_TICKS ||
(datalayer.system.settings.equipment_stop_active && contactorStatus != SHUTDOWN_REQUESTED)) { (datalayer.system.settings.equipment_stop_active && contactorStatus != SHUTDOWN_REQUESTED)) {
contactorStatus = SHUTDOWN_REQUESTED; contactorStatus = SHUTDOWN_REQUESTED;
datalayer.system.settings.equipment_stop_active = true;
} }
if (contactorStatus == SHUTDOWN_REQUESTED && !datalayer.system.settings.equipment_stop_active) { if (contactorStatus == SHUTDOWN_REQUESTED && !datalayer.system.settings.equipment_stop_active) {
contactorStatus = DISCONNECTED; contactorStatus = DISCONNECTED;
@ -856,6 +860,8 @@ void update_scaled_values() {
* Before we use real_soc, we must make sure that it's within the range of min_percentage and max_percentage. * Before we use real_soc, we must make sure that it's within the range of min_percentage and max_percentage.
*/ */
uint32_t calc_soc; uint32_t calc_soc;
uint32_t calc_max_capacity;
uint32_t calc_reserved_capacity;
// Make sure that the SOC starts out between min and max percentages // Make sure that the SOC starts out between min and max percentages
calc_soc = CONSTRAIN(datalayer.battery.status.real_soc, datalayer.battery.settings.min_percentage, calc_soc = CONSTRAIN(datalayer.battery.status.real_soc, datalayer.battery.settings.min_percentage,
datalayer.battery.settings.max_percentage); datalayer.battery.settings.max_percentage);
@ -866,8 +872,6 @@ void update_scaled_values() {
// Calculate the scaled remaining capacity in Wh // Calculate the scaled remaining capacity in Wh
if (datalayer.battery.info.total_capacity_Wh > 0 && datalayer.battery.status.real_soc > 0) { if (datalayer.battery.info.total_capacity_Wh > 0 && datalayer.battery.status.real_soc > 0) {
uint32_t calc_max_capacity;
uint32_t calc_reserved_capacity;
calc_max_capacity = (datalayer.battery.status.remaining_capacity_Wh * 10000 / datalayer.battery.status.real_soc); calc_max_capacity = (datalayer.battery.status.remaining_capacity_Wh * 10000 / datalayer.battery.status.real_soc);
calc_reserved_capacity = calc_max_capacity * datalayer.battery.settings.min_percentage / 10000; calc_reserved_capacity = calc_max_capacity * datalayer.battery.settings.min_percentage / 10000;
// remove % capacity reserved in min_percentage to total_capacity_Wh // remove % capacity reserved in min_percentage to total_capacity_Wh
@ -877,9 +881,28 @@ void update_scaled_values() {
datalayer.battery.status.reported_remaining_capacity_Wh = datalayer.battery.status.remaining_capacity_Wh; datalayer.battery.status.reported_remaining_capacity_Wh = datalayer.battery.status.remaining_capacity_Wh;
} }
#ifdef DOUBLE_BATTERY
// Calculate the scaled remaining capacity in Wh
if (datalayer.battery2.info.total_capacity_Wh > 0 && datalayer.battery2.status.real_soc > 0) {
calc_max_capacity =
(datalayer.battery2.status.remaining_capacity_Wh * 10000 / datalayer.battery2.status.real_soc);
calc_reserved_capacity = calc_max_capacity * datalayer.battery2.settings.min_percentage / 10000;
// remove % capacity reserved in min_percentage to total_capacity_Wh
datalayer.battery2.status.reported_remaining_capacity_Wh =
datalayer.battery2.status.remaining_capacity_Wh - calc_reserved_capacity;
} else {
datalayer.battery2.status.reported_remaining_capacity_Wh = datalayer.battery2.status.remaining_capacity_Wh;
}
#endif
} else { // No SOC window wanted. Set scaled to same as real. } else { // No SOC window wanted. Set scaled to same as real.
datalayer.battery.status.reported_soc = datalayer.battery.status.real_soc; datalayer.battery.status.reported_soc = datalayer.battery.status.real_soc;
datalayer.battery.status.reported_remaining_capacity_Wh = datalayer.battery.status.remaining_capacity_Wh; datalayer.battery.status.reported_remaining_capacity_Wh = datalayer.battery.status.remaining_capacity_Wh;
#ifdef DOUBLE_BATTERY
datalayer.battery2.status.reported_soc = datalayer.battery2.status.real_soc;
datalayer.battery2.status.reported_remaining_capacity_Wh = datalayer.battery2.status.remaining_capacity_Wh;
#endif
} }
#ifdef DOUBLE_BATTERY #ifdef DOUBLE_BATTERY
// Perform extra SOC sanity checks on double battery setups // Perform extra SOC sanity checks on double battery setups
@ -931,7 +954,7 @@ void runSerialDataLink() {
void init_serialDataLink() { void init_serialDataLink() {
#if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER) #if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER)
Serial2.begin(9600, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN); Serial2.begin(SERIAL_LINK_BAUDRATE, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN);
#endif #endif
} }

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

@ -422,14 +422,20 @@ void update_values_battery2() { //This function maps all the values fetched via
} else { } else {
clear_event(EVENT_HVIL_FAILURE); clear_event(EVENT_HVIL_FAILURE);
} }
if (battery2_status_precharge_locked == 2) { // Capacitor seated? if (battery2_status_error_disconnecting_switch > 0) { // Check if contactors are sticking / welded
set_event(EVENT_PRECHARGE_FAILURE, 2); set_event(EVENT_CONTACTOR_WELDED, 0);
} else { } else {
clear_event(EVENT_PRECHARGE_FAILURE); clear_event(EVENT_CONTACTOR_WELDED);
} }
} }
void update_values_battery() { //This function maps all the values fetched via CAN to the battery datalayer void update_values_battery() { //This function maps all the values fetched via CAN to the battery datalayer
if (datalayer.system.settings.equipment_stop_active == true) {
digitalWrite(WUP_PIN, LOW); // Turn off WUP_PIN
} else {
digitalWrite(WUP_PIN, HIGH); // Wake up the battery
}
if (!battery_awake) { if (!battery_awake) {
return; return;
} }
@ -484,10 +490,10 @@ void update_values_battery() { //This function maps all the values fetched via
} else { } else {
clear_event(EVENT_HVIL_FAILURE); clear_event(EVENT_HVIL_FAILURE);
} }
if (battery_status_precharge_locked == 2) { // Capacitor seated? if (battery_status_error_disconnecting_switch > 0) { // Check if contactors are sticking / welded
set_event(EVENT_PRECHARGE_FAILURE, 0); set_event(EVENT_CONTACTOR_WELDED, 0);
} else { } else {
clear_event(EVENT_PRECHARGE_FAILURE); clear_event(EVENT_CONTACTOR_WELDED);
} }
// Update webserver datalayer // Update webserver datalayer

View file

@ -6,13 +6,7 @@
#include "KIA-E-GMP-BATTERY.h" #include "KIA-E-GMP-BATTERY.h"
/* Do not change code below unless you are sure what you are doing */ /* Do not change code below unless you are sure what you are doing */
static unsigned long previousMillis10ms = 0; // will store last time a 10ms CAN Message was send
static unsigned long previousMillis20ms = 0; // will store last time a 20ms CAN Message was send
static unsigned long previousMillis30ms = 0; // will store last time a 30ms CAN Message was send
static unsigned long previousMillis100ms = 0; // will store last time a 100ms CAN Message was send
static unsigned long previousMillis200ms = 0; // will store last time a 200ms CAN Message was send static unsigned long previousMillis200ms = 0; // will store last time a 200ms CAN Message was send
static unsigned long previousMillis500ms = 0; // will store last time a 500ms CAN Message was send
static unsigned long previousMillis1s = 0; // will store last time a 1s CAN Message was send
static unsigned long previousMillis10s = 0; // will store last time a 10s CAN Message was send static unsigned long previousMillis10s = 0; // will store last time a 10s CAN Message was send
const unsigned char crc8_table[256] = const unsigned char crc8_table[256] =
@ -63,7 +57,7 @@ static uint8_t KIA_7E4_COUNTER = 0x01;
static int8_t temperature_water_inlet = 0; static int8_t temperature_water_inlet = 0;
static int8_t powerRelayTemperature = 0; static int8_t powerRelayTemperature = 0;
static int8_t heatertemp = 0; static int8_t heatertemp = 0;
static bool set_voltage_limits = false;
static uint8_t ticks_200ms_counter = 0; static uint8_t ticks_200ms_counter = 0;
static uint8_t EGMP_1CF_counter = 0; static uint8_t EGMP_1CF_counter = 0;
static uint8_t EGMP_3XF_counter = 0; static uint8_t EGMP_3XF_counter = 0;
@ -582,208 +576,7 @@ CAN_frame* messages[] = {&message_1, &message_2, &message_3, &message_4, &me
&message_43, &message_44, &message_45, &message_46, &message_47, &message_48, &message_49, &message_43, &message_44, &message_45, &message_46, &message_47, &message_48, &message_49,
&message_50, &message_51, &message_52, &message_53, &message_54, &message_55, &message_56, &message_50, &message_51, &message_52, &message_53, &message_54, &message_55, &message_56,
&message_57, &message_58, &message_59, &message_60, &message_61, &message_62, &message_63}; &message_57, &message_58, &message_59, &message_60, &message_61, &message_62, &message_63};
/* PID polling messages */
/* These messages are rest of the vehicle messages, to reduce number of active fault codes */
CAN_frame EGMP_1CF = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x1CF,
.data = {0x56, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_3AA = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x3AA,
.data = {0xFF, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00}};
CAN_frame EGMP_3E0 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x3E0,
.data = {0xC3, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_3E1 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x3E1,
.data = {0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_36F = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x36F,
.data = {0x28, 0x31, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_37F = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x37F,
.data = {0x9B, 0x30, 0x52, 0x24, 0x41, 0x02, 0x00, 0x00}};
CAN_frame EGMP_4B4 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4B4,
.data = {0x00, 0x00, 0xC0, 0x3F, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4B5 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4B5,
.data = {0x08, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4B7 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4B7,
.data = {0x08, 0x00, 0xF0, 0x07, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4CC = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4CC,
.data = {0x08, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4CE = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4CE,
.data = {0x16, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
CAN_frame EGMP_4D8 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4D8,
.data = {0x40, 0x10, 0xF0, 0xF0, 0x40, 0xF2, 0x1E, 0xCC}};
CAN_frame EGMP_4DD = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4DD,
.data = {0x3F, 0xFC, 0xFF, 0x00, 0x38, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4E7 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4E7,
.data = {0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00}};
CAN_frame EGMP_4E9 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4E9,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4EA = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4EA,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4EB = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4EB,
.data = {0x01, 0x50, 0x0B, 0x26, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4EC = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4EC,
.data = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F}};
CAN_frame EGMP_4ED = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4ED,
.data = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F}};
CAN_frame EGMP_4EE = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4EE,
.data = {0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4EF = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4EF,
.data = {0x2B, 0xFE, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00}};
CAN_frame EGMP_405 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x405,
.data = {0xE4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_410 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x410,
.data = {0xA6, 0x10, 0xFF, 0x3C, 0xFF, 0x7F, 0xFF, 0xFF}};
CAN_frame EGMP_411 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x411,
.data = {0xEA, 0x22, 0x50, 0x51, 0x00, 0x00, 0x00, 0x40}};
CAN_frame EGMP_412 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x412,
.data = {0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_413 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x413,
.data = {0xB9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_414 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x414,
.data = {0xF0, 0x10, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00}};
CAN_frame EGMP_416 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x416,
.data = {0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_417 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x417,
.data = {0xC7, 0x10, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00}};
CAN_frame EGMP_418 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x418,
.data = {0x17, 0x20, 0x00, 0x00, 0x14, 0x0C, 0x00, 0x00}};
CAN_frame EGMP_3C1 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x3C1,
.data = {0x59, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_3C2 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x3C2,
.data = {0x07, 0x00, 0x11, 0x40, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_4F0 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4F0,
.data = {0x8A, 0x0A, 0x0D, 0x34, 0x60, 0x18, 0x12, 0xFC}};
CAN_frame EGMP_4F2 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4F2,
.data = {0x0A, 0xC3, 0xD5, 0xFF, 0x0F, 0x21, 0x80, 0x2B}};
CAN_frame EGMP_4FE = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x4FE,
.data = {0x69, 0x3F, 0x00, 0x04, 0xDF, 0x01, 0x4C, 0xA8}};
CAN_frame EGMP_48F = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x48F,
.data = {0xAD, 0x10, 0x41, 0x00, 0x05, 0x00, 0x00, 0x00}};
CAN_frame EGMP_419 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x419,
.data = {0xC7, 0x90, 0xB9, 0xD2, 0x0D, 0x62, 0x7A, 0x00}};
CAN_frame EGMP_422 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x422,
.data = {0x15, 0x10, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_444 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x444,
.data = {0x96, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame EGMP_641 = {.FD = true,
.ext_ID = false,
.DLC = 8,
.ID = 0x641,
.data = {0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0xFF}};
CAN_frame EGMP_7E4 = {.FD = true, CAN_frame EGMP_7E4 = {.FD = true,
.ext_ID = false, .ext_ID = false,
.DLC = 8, .DLC = 8,
@ -804,6 +597,32 @@ void set_cell_voltages(CAN_frame rx_frame, int start, int length, int startCell)
} }
} }
void set_voltage_minmax_limits() {
uint8_t valid_cell_count = 0;
for (int i = 0; i < MAX_AMOUNT_CELLS; ++i) {
if (datalayer.battery.status.cell_voltages_mV[i] > 0) {
++valid_cell_count;
}
}
if (valid_cell_count == 144) {
datalayer.battery.info.number_of_cells = valid_cell_count;
datalayer.battery.info.max_design_voltage_dV = 6048;
datalayer.battery.info.min_design_voltage_dV = 4320;
} else if (valid_cell_count == 180) {
datalayer.battery.info.number_of_cells = valid_cell_count;
datalayer.battery.info.max_design_voltage_dV = 7560;
datalayer.battery.info.min_design_voltage_dV = 5400;
} else if (valid_cell_count == 192) {
datalayer.battery.info.number_of_cells = valid_cell_count;
datalayer.battery.info.max_design_voltage_dV = 8064;
datalayer.battery.info.min_design_voltage_dV = 5760;
} else {
// We are still starting up? Not all cells available.
set_voltage_limits = false;
}
}
static uint8_t calculateCRC(CAN_frame rx_frame, uint8_t length, uint8_t initial_value) { static uint8_t calculateCRC(CAN_frame rx_frame, uint8_t length, uint8_t initial_value) {
uint8_t crc = initial_value; uint8_t crc = initial_value;
for (uint8_t j = 1; j < length; j++) { //start at 1, since 0 is the CRC for (uint8_t j = 1; j < length; j++) { //start at 1, since 0 is the CRC
@ -826,8 +645,16 @@ void update_values_battery() { //This function maps all the values fetched via
(static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh); (static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh);
//datalayer.battery.status.max_charge_power_W = (uint16_t)allowedChargePower * 10; //From kW*100 to Watts //datalayer.battery.status.max_charge_power_W = (uint16_t)allowedChargePower * 10; //From kW*100 to Watts
//The allowed charge power is not available. We hardcode this value for now //The allowed charge power is not available. We estimate this value for now
datalayer.battery.status.max_charge_power_W = MAXCHARGEPOWERALLOWED; if (datalayer.battery.status.real_soc > 9900) {
datalayer.battery.status.max_charge_power_W = 0;
} else if (datalayer.battery.status.real_soc >
RAMPDOWN_SOC) { // When real SOC is between 90-99%, ramp the value between Max<->0
datalayer.battery.status.max_charge_power_W =
RAMPDOWNPOWERALLOWED * (1 - (datalayer.battery.status.real_soc - RAMPDOWN_SOC) / (10000.0 - RAMPDOWN_SOC));
} else { // No limits, max charging power allowed
datalayer.battery.status.max_charge_power_W = MAXCHARGEPOWERALLOWED;
}
//datalayer.battery.status.max_discharge_power_W = (uint16_t)allowedDischargePower * 10; //From kW*100 to Watts //datalayer.battery.status.max_discharge_power_W = (uint16_t)allowedDischargePower * 10; //From kW*100 to Watts
//The allowed discharge power is not available. We hardcode this value for now //The allowed discharge power is not available. We hardcode this value for now
@ -845,6 +672,11 @@ void update_values_battery() { //This function maps all the values fetched via
datalayer.battery.status.cell_min_voltage_mV = CellVoltMin_mV; datalayer.battery.status.cell_min_voltage_mV = CellVoltMin_mV;
if ((millis() > INTERVAL_60_S) && !set_voltage_limits) {
set_voltage_limits = true;
set_voltage_minmax_limits(); // Count cells, and set voltage limits accordingly
}
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!datalayer.battery.status.CAN_battery_still_alive) { if (!datalayer.battery.status.CAN_battery_still_alive) {
set_event(EVENT_CANFD_RX_FAILURE, 0); set_event(EVENT_CANFD_RX_FAILURE, 0);
@ -1178,16 +1010,16 @@ void send_can_battery() {
messageIndex = 0; messageIndex = 0;
} }
//Send 500ms CANFD message //Send 200ms CANFD message
if (currentMillis - previousMillis500ms >= INTERVAL_500_MS) { if (currentMillis - previousMillis200ms >= INTERVAL_200_MS) {
previousMillis500ms = currentMillis; previousMillis200ms = currentMillis;
// Check if sending of CAN messages has been delayed too much. // Check if sending of CAN messages has been delayed too much.
if ((currentMillis - previousMillis500ms >= INTERVAL_500_MS_DELAYED) && (currentMillis > BOOTUP_TIME)) { if ((currentMillis - previousMillis200ms >= INTERVAL_200_MS_DELAYED) && (currentMillis > BOOTUP_TIME)) {
set_event(EVENT_CAN_OVERRUN, (currentMillis - previousMillis500ms)); set_event(EVENT_CAN_OVERRUN, (currentMillis - previousMillis200ms));
} else { } else {
clear_event(EVENT_CAN_OVERRUN); clear_event(EVENT_CAN_OVERRUN);
} }
previousMillis500ms = currentMillis; previousMillis200ms = currentMillis;
EGMP_7E4.data.u8[3] = KIA_7E4_COUNTER; EGMP_7E4.data.u8[3] = KIA_7E4_COUNTER;
@ -1200,19 +1032,10 @@ void send_can_battery() {
KIA_7E4_COUNTER = 0x01; KIA_7E4_COUNTER = 0x01;
} }
} }
//Send 1s CANFD message
if (currentMillis - previousMillis1s >= INTERVAL_1_S) {
previousMillis1s = currentMillis;
/* COMMENTED OUT WHILE CONTACTOR CLOSING TESTING
transmit_can(&EGMP_48F, can_config.battery);
*/
}
//Send 10s CANFD message //Send 10s CANFD message
if (currentMillis - previousMillis10s >= INTERVAL_10_S) { if (currentMillis - previousMillis10s >= INTERVAL_10_S) {
previousMillis10s = currentMillis; previousMillis10s = currentMillis;
/* COMMENTED OUT WHILE CONTACTOR CLOSING TESTING
transmit_can(&EGMP_4FE, can_config.battery);
*/
ok_start_polling_battery = true; ok_start_polling_battery = true;
} }
} }
@ -1232,6 +1055,7 @@ void setup_battery(void) { // Performs one time setup at startup
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV; datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV; datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV; datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV;
} }
#endif #endif

View file

@ -14,6 +14,8 @@ extern ACAN2517FD canfd;
#define MIN_CELL_VOLTAGE_MV 2950 //Battery is put into emergency stop if one cell goes below this value #define MIN_CELL_VOLTAGE_MV 2950 //Battery is put into emergency stop if one cell goes below this value
#define MAXCHARGEPOWERALLOWED 10000 #define MAXCHARGEPOWERALLOWED 10000
#define MAXDISCHARGEPOWERALLOWED 10000 #define MAXDISCHARGEPOWERALLOWED 10000
#define RAMPDOWN_SOC 9000 // 90.00 SOC% to start ramping down from max charge power towards 0 at 100.00%
#define RAMPDOWNPOWERALLOWED 10000 // What power we ramp down from towards top balancing
void setup_battery(void); void setup_battery(void);
void transmit_can(CAN_frame* tx_frame, int interface); void transmit_can(CAN_frame* tx_frame, int interface);

View file

@ -67,6 +67,9 @@ void update_values_battery() {
datalayer.battery.status.max_discharge_power_W = (-max_discharge_current * (voltage_dV / 10)); datalayer.battery.status.max_discharge_power_W = (-max_discharge_current * (voltage_dV / 10));
datalayer.battery.status.remaining_capacity_Wh = static_cast<uint32_t>(
(static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh);
datalayer.battery.status.cell_max_voltage_mV = cellvoltage_max_mV; datalayer.battery.status.cell_max_voltage_mV = cellvoltage_max_mV;
datalayer.battery.status.cell_min_voltage_mV = cellvoltage_min_mV; datalayer.battery.status.cell_min_voltage_mV = cellvoltage_min_mV;

View file

@ -1,23 +1,69 @@
#include "../include.h" #include "../include.h"
#ifdef RENAULT_ZOE_GEN2_BATTERY #ifdef RENAULT_ZOE_GEN2_BATTERY
#include "../datalayer/datalayer.h" #include "../datalayer/datalayer.h"
#include "../datalayer/datalayer_extended.h" //For "More battery info" webpage
#include "../devboard/utils/events.h" #include "../devboard/utils/events.h"
#include "RENAULT-ZOE-GEN2-BATTERY.h" #include "RENAULT-ZOE-GEN2-BATTERY.h"
/* Information in this file is based of the OVMS V3 vehicle_renaultzoe.cpp component /* TODO
- Add //NVROL Reset
- Add //Enable temporisation before sleep (see ljames28 repo)
"If the pack is in a state where it is confused about the time, you may need to reset it's NVROL memory.
However, if the power is later power cycled, it will revert back to his previous confused state.
Therefore, after resetting the NVROL you must enable "temporisation before sleep", and then stop streaming 373.
It will then save the data and go to sleep. When the pack is confused, the state of charge may reset back to incorrect value
every time the power is reset which can be dangerous. In this state, the voltage will still be accurate"
*/
/* Information in this file is based on:
https://github.com/openvehicles/Open-Vehicle-Monitoring-System-3/blob/master/vehicle/OVMS.V3/components/vehicle_renaultzoe_ph2_obd/src/vehicle_renaultzoe_ph2_obd.cpp https://github.com/openvehicles/Open-Vehicle-Monitoring-System-3/blob/master/vehicle/OVMS.V3/components/vehicle_renaultzoe_ph2_obd/src/vehicle_renaultzoe_ph2_obd.cpp
https://github.com/ljames28/Renault-Zoe-PH2-ZE50-Canbus-LBC-Information?tab=readme-ov-file
https://github.com/fesch/CanZE/tree/master/app/src/main/assets/ZOE_Ph2
/* /*
/* Do not change code below unless you are sure what you are doing */ /* Do not change code below unless you are sure what you are doing */
static uint16_t LB_SOC = 50; static uint16_t battery_soc = 0;
static uint16_t LB_SOH = 99; static uint16_t battery_usable_soc = 5000;
static int16_t LB_Average_Temperature = 0; static uint16_t battery_soh = 10000;
static uint32_t LB_Charge_Power_W = 0; static uint16_t battery_pack_voltage = 370;
static int32_t LB_Current = 0; static uint16_t battery_max_cell_voltage = 3700;
static uint16_t LB_kWh_Remaining = 0; static uint16_t battery_min_cell_voltage = 3700;
static uint16_t LB_Cell_Max_Voltage = 3700; static uint16_t battery_12v = 0;
static uint16_t LB_Cell_Min_Voltage = 3700; static uint16_t battery_avg_temp = 920;
static uint16_t LB_Battery_Voltage = 3700; static uint16_t battery_min_temp = 920;
static uint16_t battery_max_temp = 920;
static uint16_t battery_max_power = 0;
static uint16_t battery_interlock = 0;
static uint16_t battery_kwh = 0;
static int32_t battery_current = 32640;
static uint16_t battery_current_offset = 0;
static uint16_t battery_max_generated = 0;
static uint16_t battery_max_available = 0;
static uint16_t battery_current_voltage = 0;
static uint16_t battery_charging_status = 0;
static uint16_t battery_remaining_charge = 0;
static uint16_t battery_balance_capacity_total = 0;
static uint16_t battery_balance_time_total = 0;
static uint16_t battery_balance_capacity_sleep = 0;
static uint16_t battery_balance_time_sleep = 0;
static uint16_t battery_balance_capacity_wake = 0;
static uint16_t battery_balance_time_wake = 0;
static uint16_t battery_bms_state = 0;
static uint16_t battery_balance_switches = 0;
static uint16_t battery_energy_complete = 0;
static uint16_t battery_energy_partial = 0;
static uint16_t battery_slave_failures = 0;
static uint16_t battery_mileage = 0;
static uint16_t battery_fan_speed = 0;
static uint16_t battery_fan_period = 0;
static uint16_t battery_fan_control = 0;
static uint16_t battery_fan_duty = 0;
static uint16_t battery_temporisation = 0;
static uint16_t battery_time = 0;
static uint16_t battery_pack_time = 0;
static uint16_t battery_soc_min = 0;
static uint16_t battery_soc_max = 0;
CAN_frame ZOE_373 = {.FD = false, CAN_frame ZOE_373 = {.FD = false,
.ext_ID = false, .ext_ID = false,
@ -28,36 +74,151 @@ CAN_frame ZOE_POLL_18DADBF1 = {.FD = false,
.ext_ID = true, .ext_ID = true,
.DLC = 8, .DLC = 8,
.ID = 0x18DADBF1, .ID = 0x18DADBF1,
.data = {0x03, 0x22, 0x90, 0x00, 0xff, 0xff, 0xff, 0xff}}; .data = {0x03, 0x22, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00}};
//NVROL Reset
CAN_frame ZOE_NVROL_1_18DADBF1 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x18DADBF1,
.data = {0x02, 0x10, 0x03, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}};
CAN_frame ZOE_NVROL_2_18DADBF1 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x18DADBF1,
.data = {0x04, 0x31, 0x01, 0xB0, 0x09, 0x00, 0xAA, 0xAA}};
//Enable temporisation before sleep
CAN_frame ZOE_SLEEP_1_18DADBF1 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x18DADBF1,
.data = {0x02, 0x10, 0x03, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}};
CAN_frame ZOE_SLEEP_2_18DADBF1 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x18DADBF1,
.data = {0x04, 0x2E, 0x92, 0x81, 0x01, 0xAA, 0xAA, 0xAA}};
const uint16_t poll_commands[41] = {POLL_SOC,
POLL_USABLE_SOC,
POLL_SOH,
POLL_PACK_VOLTAGE,
POLL_MAX_CELL_VOLTAGE,
POLL_MIN_CELL_VOLTAGE,
POLL_12V,
POLL_AVG_TEMP,
POLL_MIN_TEMP,
POLL_MAX_TEMP,
POLL_MAX_POWER,
POLL_INTERLOCK,
POLL_KWH,
POLL_CURRENT,
POLL_CURRENT_OFFSET,
POLL_MAX_GENERATED,
POLL_MAX_AVAILABLE,
POLL_CURRENT_VOLTAGE,
POLL_CHARGING_STATUS,
POLL_REMAINING_CHARGE,
POLL_BALANCE_CAPACITY_TOTAL,
POLL_BALANCE_TIME_TOTAL,
POLL_BALANCE_CAPACITY_SLEEP,
POLL_BALANCE_TIME_SLEEP,
POLL_BALANCE_CAPACITY_WAKE,
POLL_BALANCE_TIME_WAKE,
POLL_BMS_STATE,
POLL_BALANCE_SWITCHES,
POLL_ENERGY_COMPLETE,
POLL_ENERGY_PARTIAL,
POLL_SLAVE_FAILURES,
POLL_MILEAGE,
POLL_FAN_SPEED,
POLL_FAN_PERIOD,
POLL_FAN_CONTROL,
POLL_FAN_DUTY,
POLL_TEMPORISATION,
POLL_TIME,
POLL_PACK_TIME,
POLL_SOC_MIN,
POLL_SOC_MAX};
static uint8_t poll_index = 0;
static uint16_t currentpoll = POLL_SOC;
static uint16_t reply_poll = 0;
static unsigned long previousMillis200 = 0; // will store last time a 200ms CAN Message was sent static unsigned long previousMillis200 = 0; // will store last time a 200ms CAN Message was sent
void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
datalayer.battery.status.soh_pptt = (LB_SOH * 100); //Increase range from 99% -> 99.00% datalayer.battery.status.soh_pptt = battery_soh;
datalayer.battery.status.real_soc = (LB_SOC * 10); //increase LB_SOC range from 0-100.0 -> 100.00 if (battery_soc >= 300) {
datalayer.battery.status.real_soc = battery_soc - 300;
} else {
datalayer.battery.status.real_soc = 0;
}
datalayer.battery.status.voltage_dV = LB_Battery_Voltage; datalayer.battery.status.voltage_dV = battery_pack_voltage;
datalayer.battery.status.current_dA = LB_Current; datalayer.battery.status.current_dA = ((battery_current - 32640) * 0.3125);
//Calculate the remaining Wh amount from SOC% and max Wh value. //Calculate the remaining Wh amount from SOC% and max Wh value.
datalayer.battery.status.remaining_capacity_Wh = static_cast<uint32_t>( datalayer.battery.status.remaining_capacity_Wh = static_cast<uint32_t>(
(static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh); (static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh);
datalayer.battery.status.max_discharge_power_W; datalayer.battery.status.max_discharge_power_W = battery_max_available * 10;
datalayer.battery.status.max_charge_power_W; datalayer.battery.status.max_charge_power_W = battery_max_generated * 10;
datalayer.battery.status.active_power_W; datalayer.battery.status.active_power_W =
(datalayer.battery.status.current_dA * (datalayer.battery.status.voltage_dV / 100));
datalayer.battery.status.temperature_min_dC; datalayer.battery.status.temperature_min_dC = ((battery_min_temp - 640) * 0.625);
datalayer.battery.status.temperature_max_dC; datalayer.battery.status.temperature_max_dC = ((battery_max_temp - 640) * 0.625);
datalayer.battery.status.cell_min_voltage_mV; datalayer.battery.status.cell_min_voltage_mV = (battery_min_cell_voltage * 0.976563);
datalayer.battery.status.cell_max_voltage_mV; datalayer.battery.status.cell_max_voltage_mV = (battery_max_cell_voltage * 0.976563);
// Update webserver datalayer
datalayer_extended.zoePH2.battery_soc = battery_soc;
datalayer_extended.zoePH2.battery_usable_soc = battery_usable_soc;
datalayer_extended.zoePH2.battery_soh = battery_soh;
datalayer_extended.zoePH2.battery_pack_voltage = battery_pack_voltage;
datalayer_extended.zoePH2.battery_max_cell_voltage = battery_max_cell_voltage;
datalayer_extended.zoePH2.battery_min_cell_voltage = battery_min_cell_voltage;
datalayer_extended.zoePH2.battery_12v = battery_12v;
datalayer_extended.zoePH2.battery_avg_temp = battery_avg_temp;
datalayer_extended.zoePH2.battery_min_temp = battery_min_temp;
datalayer_extended.zoePH2.battery_max_temp = battery_max_temp;
datalayer_extended.zoePH2.battery_max_power = battery_max_power;
datalayer_extended.zoePH2.battery_interlock = battery_interlock;
datalayer_extended.zoePH2.battery_kwh = battery_kwh;
datalayer_extended.zoePH2.battery_current = battery_current;
datalayer_extended.zoePH2.battery_current_offset = battery_current_offset;
datalayer_extended.zoePH2.battery_max_generated = battery_max_generated;
datalayer_extended.zoePH2.battery_max_available = battery_max_available;
datalayer_extended.zoePH2.battery_current_voltage = battery_current_voltage;
datalayer_extended.zoePH2.battery_charging_status = battery_charging_status;
datalayer_extended.zoePH2.battery_remaining_charge = battery_remaining_charge;
datalayer_extended.zoePH2.battery_balance_capacity_total = battery_balance_capacity_total;
datalayer_extended.zoePH2.battery_balance_time_total = battery_balance_time_total;
datalayer_extended.zoePH2.battery_balance_capacity_sleep = battery_balance_capacity_sleep;
datalayer_extended.zoePH2.battery_balance_time_sleep = battery_balance_time_sleep;
datalayer_extended.zoePH2.battery_balance_capacity_wake = battery_balance_capacity_wake;
datalayer_extended.zoePH2.battery_balance_time_wake = battery_balance_time_wake;
datalayer_extended.zoePH2.battery_bms_state = battery_bms_state;
datalayer_extended.zoePH2.battery_balance_switches = battery_balance_switches;
datalayer_extended.zoePH2.battery_energy_complete = battery_energy_complete;
datalayer_extended.zoePH2.battery_energy_partial = battery_energy_partial;
datalayer_extended.zoePH2.battery_slave_failures = battery_slave_failures;
datalayer_extended.zoePH2.battery_mileage = battery_mileage;
datalayer_extended.zoePH2.battery_fan_speed = battery_fan_speed;
datalayer_extended.zoePH2.battery_fan_period = battery_fan_period;
datalayer_extended.zoePH2.battery_fan_control = battery_fan_control;
datalayer_extended.zoePH2.battery_fan_duty = battery_fan_duty;
datalayer_extended.zoePH2.battery_temporisation = battery_temporisation;
datalayer_extended.zoePH2.battery_time = battery_time;
datalayer_extended.zoePH2.battery_pack_time = battery_pack_time;
datalayer_extended.zoePH2.battery_soc_min = battery_soc_min;
datalayer_extended.zoePH2.battery_soc_max = battery_soc_max;
#ifdef DEBUG_VIA_USB #ifdef DEBUG_VIA_USB
@ -67,7 +228,137 @@ void update_values_battery() { //This function maps all the values fetched via
void receive_can_battery(CAN_frame rx_frame) { void receive_can_battery(CAN_frame rx_frame) {
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
switch (rx_frame.ID) { switch (rx_frame.ID) {
case 0x18daf1db: // LBC Reply from active polling case 0x18DAF1DB: // LBC Reply from active polling
//frame 2 & 3 contains
reply_poll = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3];
switch (reply_poll) {
case POLL_SOC:
battery_soc = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_USABLE_SOC:
battery_usable_soc = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_SOH:
battery_soh = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_PACK_VOLTAGE:
battery_pack_voltage = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_MAX_CELL_VOLTAGE:
battery_max_cell_voltage = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_MIN_CELL_VOLTAGE:
battery_min_cell_voltage = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_12V:
battery_12v = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_AVG_TEMP:
battery_avg_temp = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_MIN_TEMP:
battery_min_temp = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_MAX_TEMP:
battery_max_temp = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_MAX_POWER:
battery_max_power = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_INTERLOCK:
battery_interlock = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_KWH:
battery_kwh = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_CURRENT:
battery_current = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_CURRENT_OFFSET:
battery_current_offset = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_MAX_GENERATED:
battery_max_generated = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_MAX_AVAILABLE:
battery_max_available = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_CURRENT_VOLTAGE:
battery_current_voltage = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_CHARGING_STATUS:
battery_charging_status = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_REMAINING_CHARGE:
battery_remaining_charge = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_BALANCE_CAPACITY_TOTAL:
battery_balance_capacity_total = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_BALANCE_TIME_TOTAL:
battery_balance_time_total = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_BALANCE_CAPACITY_SLEEP:
battery_balance_capacity_sleep = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_BALANCE_TIME_SLEEP:
battery_balance_time_sleep = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_BALANCE_CAPACITY_WAKE:
battery_balance_capacity_wake = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_BALANCE_TIME_WAKE:
battery_balance_time_wake = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_BMS_STATE:
battery_bms_state = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_BALANCE_SWITCHES:
battery_balance_switches = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_ENERGY_COMPLETE:
battery_energy_complete = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_ENERGY_PARTIAL:
battery_energy_partial = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_SLAVE_FAILURES:
battery_slave_failures = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_MILEAGE:
battery_mileage = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_FAN_SPEED:
battery_fan_speed = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_FAN_PERIOD:
battery_fan_period = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_FAN_CONTROL:
battery_fan_control = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_FAN_DUTY:
battery_fan_duty = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_TEMPORISATION:
battery_temporisation = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_TIME:
battery_time = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_PACK_TIME:
battery_pack_time = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_SOC_MIN:
battery_soc_min = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
case POLL_SOC_MAX:
battery_soc_max = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5];
break;
default: // Unknown reply
break;
}
break; break;
default: default:
break; break;
@ -83,8 +374,16 @@ void send_can_battery() {
set_event(EVENT_CAN_OVERRUN, (currentMillis - previousMillis200)); set_event(EVENT_CAN_OVERRUN, (currentMillis - previousMillis200));
} }
previousMillis200 = currentMillis; previousMillis200 = currentMillis;
transmit_can(&ZOE_373, can_config.battery);
// Update current poll from the array
currentpoll = poll_commands[poll_index];
poll_index = (poll_index + 1) % 41;
ZOE_POLL_18DADBF1.data.u8[2] = (uint8_t)((currentpoll & 0xFF00) >> 8);
ZOE_POLL_18DADBF1.data.u8[3] = (uint8_t)(currentpoll & 0x00FF);
transmit_can(&ZOE_POLL_18DADBF1, can_config.battery); transmit_can(&ZOE_POLL_18DADBF1, can_config.battery);
transmit_can(&ZOE_373, can_config.battery);
} }
} }

View file

@ -12,4 +12,46 @@
void setup_battery(void); void setup_battery(void);
void transmit_can(CAN_frame* tx_frame, int interface); void transmit_can(CAN_frame* tx_frame, int interface);
#define POLL_SOC 0x9001
#define POLL_USABLE_SOC 0x9002
#define POLL_SOH 0x9003
#define POLL_PACK_VOLTAGE 0x9005
#define POLL_MAX_CELL_VOLTAGE 0x9007
#define POLL_MIN_CELL_VOLTAGE 0x9009
#define POLL_12V 0x9011
#define POLL_AVG_TEMP 0x9012
#define POLL_MIN_TEMP 0x9013
#define POLL_MAX_TEMP 0x9014
#define POLL_MAX_POWER 0x9018
#define POLL_INTERLOCK 0x901A
#define POLL_KWH 0x91C8
#define POLL_CURRENT 0x925D
#define POLL_CURRENT_OFFSET 0x900C
#define POLL_MAX_GENERATED 0x900E
#define POLL_MAX_AVAILABLE 0x900F
#define POLL_CURRENT_VOLTAGE 0x9130
#define POLL_CHARGING_STATUS 0x9019
#define POLL_REMAINING_CHARGE 0xF45B
#define POLL_BALANCE_CAPACITY_TOTAL 0x924F
#define POLL_BALANCE_TIME_TOTAL 0x9250
#define POLL_BALANCE_CAPACITY_SLEEP 0x9251
#define POLL_BALANCE_TIME_SLEEP 0x9252
#define POLL_BALANCE_CAPACITY_WAKE 0x9262
#define POLL_BALANCE_TIME_WAKE 0x9263
#define POLL_BMS_STATE 0x9259
#define POLL_BALANCE_SWITCHES 0x912B
#define POLL_ENERGY_COMPLETE 0x9210
#define POLL_ENERGY_PARTIAL 0x9215
#define POLL_SLAVE_FAILURES 0x9129
#define POLL_MILEAGE 0x91CF
#define POLL_FAN_SPEED 0x912E
#define POLL_FAN_PERIOD 0x91F4
#define POLL_FAN_CONTROL 0x91C9
#define POLL_FAN_DUTY 0x91F5
#define POLL_TEMPORISATION 0x9281
#define POLL_TIME 0x9261
#define POLL_PACK_TIME 0x91C1
#define POLL_SOC_MIN 0x91B9
#define POLL_SOC_MAX 0x91BA
#endif #endif

View file

@ -22,6 +22,8 @@ void print_units(char* header, int value, char* units) {
void update_values_battery() { /* This function puts fake values onto the parameters sent towards the inverter */ void update_values_battery() { /* This function puts fake values onto the parameters sent towards the inverter */
datalayer.battery.info.number_of_cells = 96;
datalayer.battery.status.real_soc = 5000; // 50.00% datalayer.battery.status.real_soc = 5000; // 50.00%
datalayer.battery.status.soh_pptt = 9900; // 99.00% datalayer.battery.status.soh_pptt = 9900; // 99.00%
@ -49,7 +51,7 @@ void update_values_battery() { /* This function puts fake values onto the parame
datalayer.battery.status.max_charge_power_W = 5000; // 5kW datalayer.battery.status.max_charge_power_W = 5000; // 5kW
for (int i = 0; i < 97; ++i) { for (int i = 0; i < 97; ++i) {
datalayer.battery.status.cell_voltages_mV[i] = 3500 + i; datalayer.battery.status.cell_voltages_mV[i] = 3700 + random(-20, 21);
} }
//Fake that we get CAN messages //Fake that we get CAN messages
@ -71,6 +73,78 @@ void update_values_battery() { /* This function puts fake values onto the parame
#endif #endif
} }
#ifdef DOUBLE_BATTERY
void update_values_battery2() { // Handle the values coming in from battery #2
datalayer.battery2.info.number_of_cells = 96;
datalayer.battery2.status.real_soc = 5000; // 50.00%
datalayer.battery2.status.soh_pptt = 9900; // 99.00%
//datalayer.battery.status.voltage_dV = 3700; // 370.0V , value set in startup in .ino file, editable via webUI
datalayer.battery2.status.current_dA = 0; // 0 A
datalayer.battery2.info.total_capacity_Wh = 30000; // 30kWh
datalayer.battery2.status.remaining_capacity_Wh = 15000; // 15kWh
datalayer.battery2.status.cell_max_voltage_mV = 3596;
datalayer.battery2.status.cell_min_voltage_mV = 3500;
datalayer.battery2.status.active_power_W = 0; // 0W
datalayer.battery2.status.temperature_min_dC = 50; // 5.0*C
datalayer.battery2.status.temperature_max_dC = 60; // 6.0*C
datalayer.battery2.status.max_discharge_power_W = 5000; // 5kW
datalayer.battery2.status.max_charge_power_W = 5000; // 5kW
for (int i = 0; i < 97; ++i) {
datalayer.battery2.status.cell_voltages_mV[i] = 3700 + random(-20, 21);
}
//Fake that we get CAN messages
datalayer.battery2.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
/*Finally print out values to serial if configured to do so*/
#ifdef DEBUG_VIA_USB
Serial.println("FAKE Values battery 2 going to inverter");
print_units("SOH 2 %: ", (datalayer.battery2.status.soh_pptt * 0.01), "% ");
print_units(", SOC 2 %: ", (datalayer.battery2.status.reported_soc * 0.01), "% ");
print_units(", Voltage 2: ", (datalayer.battery2.status.voltage_dV * 0.1), "V ");
print_units(", Max discharge power 2: ", datalayer.battery2.status.max_discharge_power_W, "W ");
print_units(", Max charge power 2: ", datalayer.battery2.status.max_charge_power_W, "W ");
print_units(", Max temp 2: ", (datalayer.battery2.status.temperature_max_dC * 0.1), "°C ");
print_units(", Min temp 2: ", (datalayer.battery2.status.temperature_min_dC * 0.1), "°C ");
print_units(", Max cell voltage 2: ", datalayer.battery2.status.cell_max_voltage_mV, "mV ");
print_units(", Min cell voltage 2: ", datalayer.battery2.status.cell_min_voltage_mV, "mV ");
Serial.println("");
#endif
}
void receive_can_battery2(CAN_frame rx_frame) {
datalayer.battery2.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
// All CAN messages recieved will be logged via serial
Serial.print(millis()); // Example printout, time, ID, length, data: 7553 1DB 8 FF C0 B9 EA 0 0 2 5D
Serial.print(" ");
Serial.print(rx_frame.ID, HEX);
Serial.print(" ");
Serial.print(rx_frame.DLC);
Serial.print(" ");
for (int i = 0; i < rx_frame.DLC; ++i) {
Serial.print(rx_frame.data.u8[i], HEX);
Serial.print(" ");
}
Serial.println("");
}
#endif // DOUBLE_BATTERY
void receive_can_battery(CAN_frame rx_frame) { void receive_can_battery(CAN_frame rx_frame) {
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
// All CAN messages recieved will be logged via serial // All CAN messages recieved will be logged via serial
@ -97,6 +171,8 @@ void send_can_battery() {
} }
void setup_battery(void) { // Performs one time setup at startup void setup_battery(void) { // Performs one time setup at startup
randomSeed(analogRead(0));
#ifdef DEBUG_VIA_USB #ifdef DEBUG_VIA_USB
Serial.println("Test mode with fake battery selected"); Serial.println("Test mode with fake battery selected");
#endif #endif

View file

@ -216,6 +216,52 @@ typedef struct {
} DATALAYER_INFO_NISSAN_LEAF; } DATALAYER_INFO_NISSAN_LEAF;
typedef struct {
/** uint16_t */
/** Values WIP*/
uint16_t battery_soc = 0;
uint16_t battery_usable_soc = 0;
uint16_t battery_soh = 0;
uint16_t battery_pack_voltage = 0;
uint16_t battery_max_cell_voltage = 0;
uint16_t battery_min_cell_voltage = 0;
uint16_t battery_12v = 0;
uint16_t battery_avg_temp = 0;
uint16_t battery_min_temp = 0;
uint16_t battery_max_temp = 0;
uint16_t battery_max_power = 0;
uint16_t battery_interlock = 0;
uint16_t battery_kwh = 0;
uint16_t battery_current = 0;
uint16_t battery_current_offset = 0;
uint16_t battery_max_generated = 0;
uint16_t battery_max_available = 0;
uint16_t battery_current_voltage = 0;
uint16_t battery_charging_status = 0;
uint16_t battery_remaining_charge = 0;
uint16_t battery_balance_capacity_total = 0;
uint16_t battery_balance_time_total = 0;
uint16_t battery_balance_capacity_sleep = 0;
uint16_t battery_balance_time_sleep = 0;
uint16_t battery_balance_capacity_wake = 0;
uint16_t battery_balance_time_wake = 0;
uint16_t battery_bms_state = 0;
uint16_t battery_balance_switches = 0;
uint16_t battery_energy_complete = 0;
uint16_t battery_energy_partial = 0;
uint16_t battery_slave_failures = 0;
uint16_t battery_mileage = 0;
uint16_t battery_fan_speed = 0;
uint16_t battery_fan_period = 0;
uint16_t battery_fan_control = 0;
uint16_t battery_fan_duty = 0;
uint16_t battery_temporisation = 0;
uint16_t battery_time = 0;
uint16_t battery_pack_time = 0;
uint16_t battery_soc_min = 0;
uint16_t battery_soc_max = 0;
} DATALAYER_INFO_ZOE_PH2;
class DataLayerExtended { class DataLayerExtended {
public: public:
DATALAYER_INFO_BMWI3 bmwi3; DATALAYER_INFO_BMWI3 bmwi3;
@ -223,6 +269,7 @@ class DataLayerExtended {
DATALAYER_INFO_CELLPOWER cellpower; DATALAYER_INFO_CELLPOWER cellpower;
DATALAYER_INFO_TESLA tesla; DATALAYER_INFO_TESLA tesla;
DATALAYER_INFO_NISSAN_LEAF nissanleaf; DATALAYER_INFO_NISSAN_LEAF nissanleaf;
DATALAYER_INFO_ZOE_PH2 zoePH2;
}; };
extern DataLayerExtended datalayer_extended; extern DataLayerExtended datalayer_extended;

View file

@ -66,15 +66,35 @@ SensorConfig sensorConfigs[] = {
{"max_charge_power", "Battery Max Charge Power", "{{ value_json.max_charge_power }}", "W", "power"}, {"max_charge_power", "Battery Max Charge Power", "{{ value_json.max_charge_power }}", "W", "power"},
{"bms_status", "BMS Status", "{{ value_json.bms_status }}", "", ""}, {"bms_status", "BMS Status", "{{ value_json.bms_status }}", "", ""},
{"pause_status", "Pause Status", "{{ value_json.pause_status }}", "", ""}, {"pause_status", "Pause Status", "{{ value_json.pause_status }}", "", ""},
#ifdef DOUBLE_BATTERY
{"SOC_2", "SOC 2 (scaled)", "{{ value_json.SOC_2 }}", "%", "battery"},
{"SOC_real_2", "SOC 2 (real)", "{{ value_json.SOC_real_2 }}", "%", "battery"},
{"state_of_health_2", "State Of Health 2", "{{ value_json.state_of_health_2 }}", "%", "battery"},
{"temperature_min_2", "Temperature Min 2", "{{ value_json.temperature_min_2 }}", "°C", "temperature"},
{"temperature_max_2", "Temperature Max 2", "{{ value_json.temperature_max_2 }}", "°C", "temperature"},
{"stat_batt_power_2", "Stat Batt Power 2", "{{ value_json.stat_batt_power_2 }}", "W", "power"},
{"battery_current_2", "Battery 2 Current", "{{ value_json.battery_current_2 }}", "A", "current"},
{"cell_max_voltage_2", "Cell Max Voltage 2", "{{ value_json.cell_max_voltage_2 }}", "V", "voltage"},
{"cell_min_voltage_2", "Cell Min Voltage 2", "{{ value_json.cell_min_voltage_2 }}", "V", "voltage"},
{"battery_voltage_2", "Battery 2 Voltage", "{{ value_json.battery_voltage_2 }}", "V", "voltage"},
{"total_capacity_2", "Battery 2 Total Capacity", "{{ value_json.total_capacity_2 }}", "Wh", "energy"},
{"remaining_capacity_2", "Battery 2 Remaining Capacity (scaled)", "{{ value_json.remaining_capacity_2 }}", "Wh",
"energy"},
{"remaining_capacity_real_2", "Battery 2 Remaining Capacity (real)", "{{ value_json.remaining_capacity_real_2 }}",
"Wh", "energy"},
{"max_discharge_power_2", "Battery 2 Max Discharge Power", "{{ value_json.max_discharge_power_2 }}", "W", "power"},
{"max_charge_power_2", "Battery 2 Max Charge Power", "{{ value_json.max_charge_power_2 }}", "W", "power"},
{"bms_status_2", "BMS 2 Status", "{{ value_json.bms_status_2 }}", "", ""},
{"pause_status_2", "Pause Status 2", "{{ value_json.pause_status_2 }}", "", ""},
#endif // DOUBLE_BATTERY
}; };
static String generateCommonInfoAutoConfigTopic(const char* object_id) { static String generateCommonInfoAutoConfigTopic(const char* object_id) {
return "homeassistant/sensor/" + topic_name + "/" + String(object_id) + "/config"; return "homeassistant/sensor/" + topic_name + "/" + String(object_id) + "/config";
} }
static String generateCellVoltageAutoConfigTopic(int cell_number) { static String generateCellVoltageAutoConfigTopic(int cell_number, String battery_suffix) {
return "homeassistant/sensor/" + topic_name + "/cell_voltage" + String(cell_number) + "/config"; return "homeassistant/sensor/" + topic_name + "/cell_voltage" + battery_suffix + String(cell_number) + "/config";
} }
static String generateEventsAutoConfigTopic(const char* object_id) { static String generateEventsAutoConfigTopic(const char* object_id) {
@ -127,7 +147,7 @@ static void publish_common_info(void) {
doc["pause_status"] = get_emulator_pause_status(); doc["pause_status"] = get_emulator_pause_status();
//only publish these values if BMS is active and we are comunication with the battery (can send CAN messages to the battery) //only publish these values if BMS is active and we are comunication with the battery (can send CAN messages to the battery)
if (datalayer.battery.status.bms_status == ACTIVE && allowed_to_send_CAN && millis() > BOOTUP_TIME) { if (datalayer.battery.status.CAN_battery_still_alive && allowed_to_send_CAN && millis() > BOOTUP_TIME) {
doc["SOC"] = ((float)datalayer.battery.status.reported_soc) / 100.0; doc["SOC"] = ((float)datalayer.battery.status.reported_soc) / 100.0;
doc["SOC_real"] = ((float)datalayer.battery.status.real_soc) / 100.0; doc["SOC_real"] = ((float)datalayer.battery.status.real_soc) / 100.0;
doc["state_of_health"] = ((float)datalayer.battery.status.soh_pptt) / 100.0; doc["state_of_health"] = ((float)datalayer.battery.status.soh_pptt) / 100.0;
@ -148,7 +168,30 @@ static void publish_common_info(void) {
doc["max_discharge_power"] = ((float)datalayer.battery.status.max_discharge_power_W); doc["max_discharge_power"] = ((float)datalayer.battery.status.max_discharge_power_W);
doc["max_charge_power"] = ((float)datalayer.battery.status.max_charge_power_W); doc["max_charge_power"] = ((float)datalayer.battery.status.max_charge_power_W);
} }
#ifdef DOUBLE_BATTERY
//only publish these values if BMS is active and we are comunication with the battery (can send CAN messages to the battery)
if (datalayer.battery2.status.CAN_battery_still_alive && allowed_to_send_CAN && millis() > BOOTUP_TIME) {
doc["SOC_2"] = ((float)datalayer.battery2.status.reported_soc) / 100.0;
doc["SOC_real_2"] = ((float)datalayer.battery2.status.real_soc) / 100.0;
doc["state_of_health_2"] = ((float)datalayer.battery2.status.soh_pptt) / 100.0;
doc["temperature_min_2"] = ((float)((int16_t)datalayer.battery2.status.temperature_min_dC)) / 10.0;
doc["temperature_max_2"] = ((float)((int16_t)datalayer.battery2.status.temperature_max_dC)) / 10.0;
doc["stat_batt_power_2"] = ((float)((int32_t)datalayer.battery2.status.active_power_W));
doc["battery_current_2"] = ((float)((int16_t)datalayer.battery2.status.current_dA)) / 10.0;
doc["battery_voltage_2"] = ((float)datalayer.battery2.status.voltage_dV) / 10.0;
// publish only if cell voltages have been populated...
if (datalayer.battery2.info.number_of_cells != 0u &&
datalayer.battery2.status.cell_voltages_mV[datalayer.battery2.info.number_of_cells - 1] != 0u) {
doc["cell_max_voltage_2"] = ((float)datalayer.battery2.status.cell_max_voltage_mV) / 1000.0;
doc["cell_min_voltage_2"] = ((float)datalayer.battery2.status.cell_min_voltage_mV) / 1000.0;
}
doc["total_capacity_2"] = ((float)datalayer.battery2.info.total_capacity_Wh);
doc["remaining_capacity_real_2"] = ((float)datalayer.battery2.status.remaining_capacity_Wh);
doc["remaining_capacity_2"] = ((float)datalayer.battery2.status.reported_remaining_capacity_Wh);
doc["max_discharge_power_2"] = ((float)datalayer.battery2.status.max_discharge_power_W);
doc["max_charge_power_2"] = ((float)datalayer.battery2.status.max_charge_power_W);
}
#endif // DOUBLE_BATTERY
serializeJson(doc, mqtt_msg); serializeJson(doc, mqtt_msg);
if (!mqtt_publish(state_topic.c_str(), mqtt_msg, false)) { if (!mqtt_publish(state_topic.c_str(), mqtt_msg, false)) {
#ifdef DEBUG_VIA_USB #ifdef DEBUG_VIA_USB
@ -167,47 +210,79 @@ static void publish_cell_voltages(void) {
#endif // HA_AUTODISCOVERY #endif // HA_AUTODISCOVERY
static JsonDocument doc; static JsonDocument doc;
static String state_topic = topic_name + "/spec_data"; static String state_topic = topic_name + "/spec_data";
#ifdef DOUBLE_BATTERY
static String state_topic_2 = topic_name + "/spec_data_2";
#endif // DOUBLE_BATTERY
// If the cell voltage number isn't initialized...
if (datalayer.battery.info.number_of_cells == 0u) {
return;
}
#ifdef HA_AUTODISCOVERY #ifdef HA_AUTODISCOVERY
if (mqtt_first_transmission == true) { if (mqtt_first_transmission == true) {
mqtt_first_transmission = false; mqtt_first_transmission = false;
String topic = "homeassistant/sensor/battery-emulator/cell_voltage";
for (int i = 0; i < datalayer.battery.info.number_of_cells; i++) { // If the cell voltage number isn't initialized...
int cellNumber = i + 1; if (datalayer.battery.info.number_of_cells != 0u) {
doc["name"] = "Battery Cell Voltage " + String(cellNumber);
doc["object_id"] = object_id_prefix + "battery_voltage_cell" + String(cellNumber);
doc["unique_id"] = topic_name + "_battery_voltage_cell" + String(cellNumber);
doc["device_class"] = "voltage";
doc["state_class"] = "measurement";
doc["state_topic"] = state_topic;
doc["unit_of_measurement"] = "V";
doc["enabled_by_default"] = true;
doc["expire_after"] = 240;
doc["value_template"] = "{{ value_json.cell_voltages[" + String(i) + "] }}";
doc["device"]["identifiers"][0] = "battery-emulator";
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = device_name;
doc["origin"]["name"] = "BatteryEmulator";
doc["origin"]["sw"] = String(version_number) + "-mqtt";
doc["origin"]["url"] = "https://github.com/dalathegreat/Battery-Emulator";
serializeJson(doc, mqtt_msg, sizeof(mqtt_msg)); for (int i = 0; i < datalayer.battery.info.number_of_cells; i++) {
mqtt_publish(generateCellVoltageAutoConfigTopic(cellNumber).c_str(), mqtt_msg, true); int cellNumber = i + 1;
doc["name"] = "Battery Cell Voltage " + String(cellNumber);
doc["object_id"] = object_id_prefix + "battery_voltage_cell" + String(cellNumber);
doc["unique_id"] = topic_name + "_battery_voltage_cell" + String(cellNumber);
doc["device_class"] = "voltage";
doc["state_class"] = "measurement";
doc["state_topic"] = state_topic;
doc["unit_of_measurement"] = "V";
doc["enabled_by_default"] = true;
doc["expire_after"] = 240;
doc["value_template"] = "{{ value_json.cell_voltages[" + String(i) + "] }}";
doc["device"]["identifiers"][0] = "battery-emulator";
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = device_name;
doc["origin"]["name"] = "BatteryEmulator";
doc["origin"]["sw"] = String(version_number) + "-mqtt";
doc["origin"]["url"] = "https://github.com/dalathegreat/Battery-Emulator";
serializeJson(doc, mqtt_msg, sizeof(mqtt_msg));
mqtt_publish(generateCellVoltageAutoConfigTopic(cellNumber, "").c_str(), mqtt_msg, true);
}
doc.clear(); // clear after sending autoconfig
} }
doc.clear(); // clear after sending autoconfig #ifdef DOUBLE_BATTERY
} else { // If the cell voltage number isn't initialized...
if (datalayer.battery2.info.number_of_cells != 0u) {
for (int i = 0; i < datalayer.battery.info.number_of_cells; i++) {
int cellNumber = i + 1;
doc["name"] = "Battery 2 Cell Voltage " + String(cellNumber);
doc["object_id"] = object_id_prefix + "battery_2_voltage_cell" + String(cellNumber);
doc["unique_id"] = topic_name + "_battery_2_voltage_cell" + String(cellNumber);
doc["device_class"] = "voltage";
doc["state_class"] = "measurement";
doc["state_topic"] = state_topic_2;
doc["unit_of_measurement"] = "V";
doc["enabled_by_default"] = true;
doc["expire_after"] = 240;
doc["value_template"] = "{{ value_json.cell_voltages[" + String(i) + "] }}";
doc["device"]["identifiers"][0] = "battery-emulator";
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = device_name;
doc["origin"]["name"] = "BatteryEmulator";
doc["origin"]["sw"] = String(version_number) + "-mqtt";
doc["origin"]["url"] = "https://github.com/dalathegreat/Battery-Emulator";
serializeJson(doc, mqtt_msg, sizeof(mqtt_msg));
mqtt_publish(generateCellVoltageAutoConfigTopic(cellNumber, "_2_").c_str(), mqtt_msg, true);
}
doc.clear(); // clear after sending autoconfig
}
#endif // DOUBLE_BATTERY
}
#endif // HA_AUTODISCOVERY #endif // HA_AUTODISCOVERY
// If cell voltages haven't been populated...
if (datalayer.battery.info.number_of_cells == 0u || // If cell voltages have been populated...
datalayer.battery.status.cell_voltages_mV[datalayer.battery.info.number_of_cells - 1] == 0u) { if (datalayer.battery.info.number_of_cells != 0u &&
return; datalayer.battery.status.cell_voltages_mV[datalayer.battery.info.number_of_cells - 1] != 0u) {
}
JsonArray cell_voltages = doc["cell_voltages"].to<JsonArray>(); JsonArray cell_voltages = doc["cell_voltages"].to<JsonArray>();
for (size_t i = 0; i < datalayer.battery.info.number_of_cells; ++i) { for (size_t i = 0; i < datalayer.battery.info.number_of_cells; ++i) {
@ -222,9 +297,28 @@ static void publish_cell_voltages(void) {
#endif // DEBUG_VIA_USB #endif // DEBUG_VIA_USB
} }
doc.clear(); doc.clear();
#ifdef HA_AUTODISCOVERY
} }
#endif // HA_AUTODISCOVERY
#ifdef DOUBLE_BATTERY
// If cell voltages have been populated...
if (datalayer.battery2.info.number_of_cells != 0u &&
datalayer.battery2.status.cell_voltages_mV[datalayer.battery2.info.number_of_cells - 1] != 0u) {
JsonArray cell_voltages = doc["cell_voltages"].to<JsonArray>();
for (size_t i = 0; i < datalayer.battery2.info.number_of_cells; ++i) {
cell_voltages.add(((float)datalayer.battery2.status.cell_voltages_mV[i]) / 1000.0);
}
serializeJson(doc, mqtt_msg, sizeof(mqtt_msg));
if (!mqtt_publish(state_topic_2.c_str(), mqtt_msg, false)) {
#ifdef DEBUG_VIA_USB
Serial.println("Cell voltage MQTT msg could not be sent");
#endif // DEBUG_VIA_USB
}
doc.clear();
}
#endif // DOUBLE_BATTERY
} }
void publish_events() { void publish_events() {

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

@ -150,6 +150,7 @@ void init_events(void) {
events.entries[EVENT_CAN_RX_WARNING].level = EVENT_LEVEL_WARNING; events.entries[EVENT_CAN_RX_WARNING].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CAN_TX_FAILURE].level = EVENT_LEVEL_ERROR; events.entries[EVENT_CAN_TX_FAILURE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CAN_INVERTER_MISSING].level = EVENT_LEVEL_WARNING; events.entries[EVENT_CAN_INVERTER_MISSING].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CONTACTOR_WELDED].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_WATER_INGRESS].level = EVENT_LEVEL_ERROR; events.entries[EVENT_WATER_INGRESS].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CHARGE_LIMIT_EXCEEDED].level = EVENT_LEVEL_INFO; events.entries[EVENT_CHARGE_LIMIT_EXCEEDED].level = EVENT_LEVEL_INFO;
events.entries[EVENT_DISCHARGE_LIMIT_EXCEEDED].level = EVENT_LEVEL_INFO; events.entries[EVENT_DISCHARGE_LIMIT_EXCEEDED].level = EVENT_LEVEL_INFO;
@ -281,6 +282,8 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
return "ERROR: CAN messages failed to transmit, or no one on the bus to ACK the message!"; return "ERROR: CAN messages failed to transmit, or no one on the bus to ACK the message!";
case EVENT_CAN_INVERTER_MISSING: case EVENT_CAN_INVERTER_MISSING:
return "Warning: Inverter not sending messages on CAN bus. Check wiring!"; return "Warning: Inverter not sending messages on CAN bus. Check wiring!";
case EVENT_CONTACTOR_WELDED:
return "Warning: Contactors sticking/welded. Inspect battery with caution!";
case EVENT_CHARGE_LIMIT_EXCEEDED: case EVENT_CHARGE_LIMIT_EXCEEDED:
return "Info: Inverter is charging faster than battery is allowing."; return "Info: Inverter is charging faster than battery is allowing.";
case EVENT_DISCHARGE_LIMIT_EXCEEDED: case EVENT_DISCHARGE_LIMIT_EXCEEDED:

View file

@ -6,7 +6,7 @@
// #define INCLUDE_EVENTS_TEST // Enable to run an event test loop, see events_test_on_target.cpp // #define INCLUDE_EVENTS_TEST // Enable to run an event test loop, see events_test_on_target.cpp
#define EE_MAGIC_HEADER_VALUE 0x0016 // 0x0000 to 0xFFFF #define EE_MAGIC_HEADER_VALUE 0x0017 // 0x0000 to 0xFFFF
#define GENERATE_ENUM(ENUM) ENUM, #define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING, #define GENERATE_STRING(STRING) #STRING,
@ -37,6 +37,7 @@
XX(EVENT_CAN_TX_FAILURE) \ XX(EVENT_CAN_TX_FAILURE) \
XX(EVENT_CAN_INVERTER_MISSING) \ XX(EVENT_CAN_INVERTER_MISSING) \
XX(EVENT_CHARGE_LIMIT_EXCEEDED) \ XX(EVENT_CHARGE_LIMIT_EXCEEDED) \
XX(EVENT_CONTACTOR_WELDED) \
XX(EVENT_DISCHARGE_LIMIT_EXCEEDED) \ XX(EVENT_DISCHARGE_LIMIT_EXCEEDED) \
XX(EVENT_WATER_INGRESS) \ XX(EVENT_WATER_INGRESS) \
XX(EVENT_12V_LOW) \ XX(EVENT_12V_LOW) \

View file

@ -299,8 +299,55 @@ String advanced_battery_processor(const String& var) {
content += "<h4>Challenge failed: " + String(datalayer_extended.nissanleaf.challengeFailed) + "</h4>"; content += "<h4>Challenge failed: " + String(datalayer_extended.nissanleaf.challengeFailed) + "</h4>";
#endif #endif
#ifdef RENAULT_ZOE_GEN2_BATTERY
content += "<h4>soc: " + String(datalayer_extended.zoePH2.battery_soc) + "</h4>";
content += "<h4>usable soc: " + String(datalayer_extended.zoePH2.battery_usable_soc) + "</h4>";
content += "<h4>soh: " + String(datalayer_extended.zoePH2.battery_soh) + "</h4>";
content += "<h4>pack voltage: " + String(datalayer_extended.zoePH2.battery_pack_voltage) + "</h4>";
content += "<h4>max cell voltage: " + String(datalayer_extended.zoePH2.battery_max_cell_voltage) + "</h4>";
content += "<h4>min cell voltage: " + String(datalayer_extended.zoePH2.battery_min_cell_voltage) + "</h4>";
content += "<h4>12v: " + String(datalayer_extended.zoePH2.battery_12v) + "</h4>";
content += "<h4>avg temp: " + String(datalayer_extended.zoePH2.battery_avg_temp) + "</h4>";
content += "<h4>min temp: " + String(datalayer_extended.zoePH2.battery_min_temp) + "</h4>";
content += "<h4>max temp: " + String(datalayer_extended.zoePH2.battery_max_temp) + "</h4>";
content += "<h4>max power: " + String(datalayer_extended.zoePH2.battery_max_power) + "</h4>";
content += "<h4>interlock: " + String(datalayer_extended.zoePH2.battery_interlock) + "</h4>";
content += "<h4>kwh: " + String(datalayer_extended.zoePH2.battery_kwh) + "</h4>";
content += "<h4>current: " + String(datalayer_extended.zoePH2.battery_current) + "</h4>";
content += "<h4>current offset: " + String(datalayer_extended.zoePH2.battery_current_offset) + "</h4>";
content += "<h4>max generated: " + String(datalayer_extended.zoePH2.battery_max_generated) + "</h4>";
content += "<h4>max available: " + String(datalayer_extended.zoePH2.battery_max_available) + "</h4>";
content += "<h4>current voltage: " + String(datalayer_extended.zoePH2.battery_current_voltage) + "</h4>";
content += "<h4>charging status: " + String(datalayer_extended.zoePH2.battery_charging_status) + "</h4>";
content += "<h4>remaining charge: " + String(datalayer_extended.zoePH2.battery_remaining_charge) + "</h4>";
content +=
"<h4>balance capacity total: " + String(datalayer_extended.zoePH2.battery_balance_capacity_total) + "</h4>";
content += "<h4>balance time total: " + String(datalayer_extended.zoePH2.battery_balance_time_total) + "</h4>";
content +=
"<h4>balance capacity sleep: " + String(datalayer_extended.zoePH2.battery_balance_capacity_sleep) + "</h4>";
content += "<h4>balance time sleep: " + String(datalayer_extended.zoePH2.battery_balance_time_sleep) + "</h4>";
content +=
"<h4>balance capacity wake: " + String(datalayer_extended.zoePH2.battery_balance_capacity_wake) + "</h4>";
content += "<h4>balance time wake: " + String(datalayer_extended.zoePH2.battery_balance_time_wake) + "</h4>";
content += "<h4>bms state: " + String(datalayer_extended.zoePH2.battery_bms_state) + "</h4>";
content += "<h4>balance switches: " + String(datalayer_extended.zoePH2.battery_balance_switches) + "</h4>";
content += "<h4>energy complete: " + String(datalayer_extended.zoePH2.battery_energy_complete) + "</h4>";
content += "<h4>energy partial: " + String(datalayer_extended.zoePH2.battery_energy_partial) + "</h4>";
content += "<h4>slave failures: " + String(datalayer_extended.zoePH2.battery_slave_failures) + "</h4>";
content += "<h4>mileage: " + String(datalayer_extended.zoePH2.battery_mileage) + "</h4>";
content += "<h4>fan speed: " + String(datalayer_extended.zoePH2.battery_fan_speed) + "</h4>";
content += "<h4>fan period: " + String(datalayer_extended.zoePH2.battery_fan_period) + "</h4>";
content += "<h4>fan control: " + String(datalayer_extended.zoePH2.battery_fan_control) + "</h4>";
content += "<h4>fan duty: " + String(datalayer_extended.zoePH2.battery_fan_duty) + "</h4>";
content += "<h4>temporisation: " + String(datalayer_extended.zoePH2.battery_temporisation) + "</h4>";
content += "<h4>time: " + String(datalayer_extended.zoePH2.battery_time) + "</h4>";
content += "<h4>pack time: " + String(datalayer_extended.zoePH2.battery_pack_time) + "</h4>";
content += "<h4>soc min: " + String(datalayer_extended.zoePH2.battery_soc_min) + "</h4>";
content += "<h4>soc max: " + String(datalayer_extended.zoePH2.battery_soc_max) + "</h4>";
#endif //RENAULT_ZOE_GEN2_BATTERY
#if !defined(TESLA_BATTERY) && !defined(NISSAN_LEAF_BATTERY) && !defined(BMW_I3_BATTERY) && \ #if !defined(TESLA_BATTERY) && !defined(NISSAN_LEAF_BATTERY) && !defined(BMW_I3_BATTERY) && \
!defined(BYD_ATTO_3_BATTERY) && !defined(CELLPOWER_BMS) //Only the listed types have extra info !defined(BYD_ATTO_3_BATTERY) && !defined(RENAULT_ZOE_GEN2_BATTERY) && !defined(CELLPOWER_BMS)
content += "No extra information available for this battery type"; content += "No extra information available for this battery type";
#endif #endif

View file

@ -505,6 +505,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
@ -729,6 +732,27 @@ String processor(const String& var) {
content += "<span style='color: red;'>OFF</span>"; content += "<span style='color: red;'>OFF</span>";
} }
content += "</h4>"; content += "</h4>";
content += "<h4>Pre Charge: ";
if (digitalRead(PRECHARGE_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Neg.: ";
if (digitalRead(NEGATIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Pos.: ";
if (digitalRead(POSITIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += "</h4>";
#endif #endif
// Close the block // Close the block
@ -771,7 +795,9 @@ String processor(const String& var) {
content += "<h4 style='color: white;'>Current: " + String(currentFloat, 1) + " A</h4>"; content += "<h4 style='color: white;'>Current: " + String(currentFloat, 1) + " A</h4>";
content += formatPowerValue("Power", powerFloat, "", 1); content += formatPowerValue("Power", powerFloat, "", 1);
content += formatPowerValue("Total capacity", datalayer.battery2.info.total_capacity_Wh, "h", 0); content += formatPowerValue("Total capacity", datalayer.battery2.info.total_capacity_Wh, "h", 0);
content += formatPowerValue("Remaining capacity", datalayer.battery2.status.remaining_capacity_Wh, "h", 1); content += formatPowerValue("Real Remaining capacity", datalayer.battery2.status.remaining_capacity_Wh, "h", 1);
content +=
formatPowerValue("Scaled Remaining capacity", datalayer.battery2.status.reported_remaining_capacity_Wh, "h", 1);
content += formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1); content += formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1);
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1); content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1);
content += "<h4>Cell max: " + String(datalayer.battery2.status.cell_max_voltage_mV) + " mV</h4>"; content += "<h4>Cell max: " + String(datalayer.battery2.status.cell_max_voltage_mV) + " mV</h4>";
@ -813,6 +839,11 @@ String processor(const String& var) {
content += "<span style='color: red;'>&#10005;</span></h4>"; content += "<span style='color: red;'>&#10005;</span></h4>";
} }
if (emulator_pause_status == NORMAL)
content += "<h4>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
else
content += "<h4 style='color: red;'>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
#ifdef CONTACTOR_CONTROL #ifdef CONTACTOR_CONTROL
content += "<h4>Contactors controlled by Battery-Emulator: "; content += "<h4>Contactors controlled by Battery-Emulator: ";
if (datalayer.system.status.contactor_control_closed) { if (datalayer.system.status.contactor_control_closed) {
@ -821,12 +852,28 @@ String processor(const String& var) {
content += "<span style='color: red;'>OFF</span>"; content += "<span style='color: red;'>OFF</span>";
} }
content += "</h4>"; content += "</h4>";
#endif
if (emulator_pause_status == NORMAL) content += "<h4>Pre Charge: ";
content += "<h4>Pause status: " + String(get_emulator_pause_status().c_str()) + " </h4>"; if (digitalRead(PRECHARGE_PIN) == HIGH) {
else content += "<span style='color: green;'>&#10003;</span>";
content += "<h4 style='color: red;'>Pause status: " + String(get_emulator_pause_status().c_str()) + " </h4>"; } else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Neg.: ";
if (digitalRead(NEGATIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Pos.: ";
if (digitalRead(POSITIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += "</h4>";
#endif
content += "</div>"; content += "</div>";
content += "</div>"; content += "</div>";

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