Merge pull request #1487 from dalathegreat/improvement/estimated-charge

Feature: Add manual override charge/discharge limits
This commit is contained in:
Daniel Öster 2025-09-04 20:34:33 +03:00 committed by GitHub
commit a8532b6f78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 66 additions and 34 deletions

View file

@ -60,7 +60,7 @@ void BmwIXBattery::update_values() { //This function maps all the values fetche
datalayer.battery.status.soh_pptt = min_soh_state; datalayer.battery.status.soh_pptt = min_soh_state;
datalayer.battery.status.max_discharge_power_W = MAX_DISCHARGE_POWER_ALLOWED_W; datalayer.battery.status.max_discharge_power_W = datalayer.battery.status.override_discharge_power_W;
//datalayer.battery.status.max_charge_power_W = 3200; //10000; //Aux HV Port has 100A Fuse Moved to Ramping //datalayer.battery.status.max_charge_power_W = 3200; //10000; //Aux HV Port has 100A Fuse Moved to Ramping
@ -70,10 +70,10 @@ void BmwIXBattery::update_values() { //This function maps all the values fetche
} else if (datalayer.battery.status.real_soc > RAMPDOWN_SOC) { } else if (datalayer.battery.status.real_soc > RAMPDOWN_SOC) {
// When real SOC is between RAMPDOWN_SOC-99%, ramp the value between Max<->0 // When real SOC is between RAMPDOWN_SOC-99%, ramp the value between Max<->0
datalayer.battery.status.max_charge_power_W = datalayer.battery.status.max_charge_power_W =
MAX_CHARGE_POWER_ALLOWED_W * datalayer.battery.status.override_charge_power_W *
(1 - (datalayer.battery.status.real_soc - RAMPDOWN_SOC) / (10000.0 - RAMPDOWN_SOC)); (1 - (datalayer.battery.status.real_soc - RAMPDOWN_SOC) / (10000.0 - RAMPDOWN_SOC));
} else { // No limits, max charging power allowed } else { // No limits, max charging power allowed
datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_ALLOWED_W; datalayer.battery.status.max_charge_power_W = datalayer.battery.status.override_charge_power_W;
} }
datalayer.battery.status.temperature_min_dC = min_battery_temperature; datalayer.battery.status.temperature_min_dC = min_battery_temperature;

View file

@ -48,8 +48,6 @@ class BmwIXBattery : public CanBattery {
static const int MAX_CELL_DEVIATION_MV = 250; static const int MAX_CELL_DEVIATION_MV = 250;
static const int MAX_CELL_VOLTAGE_MV = 4300; //Battery is put into emergency stop if one cell goes over this value static const int MAX_CELL_VOLTAGE_MV = 4300; //Battery is put into emergency stop if one cell goes over this value
static const int MIN_CELL_VOLTAGE_MV = 2800; //Battery is put into emergency stop if one cell goes below this value static const int MIN_CELL_VOLTAGE_MV = 2800; //Battery is put into emergency stop if one cell goes below this value
static const int MAX_DISCHARGE_POWER_ALLOWED_W = 10000;
static const int MAX_CHARGE_POWER_ALLOWED_W = 10000;
static const int MAX_CHARGE_POWER_WHEN_TOPBALANCING_W = 500; static const int MAX_CHARGE_POWER_WHEN_TOPBALANCING_W = 500;
static const int RAMPDOWN_SOC = static const int RAMPDOWN_SOC =
9000; // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00% 9000; // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00%

View file

@ -96,14 +96,14 @@ void BoltAmperaBattery::update_values() { //This function maps all the values f
} else if (datalayer.battery.status.real_soc > RAMPDOWN_SOC) { } else if (datalayer.battery.status.real_soc > RAMPDOWN_SOC) {
// When real SOC is between RAMPDOWN_SOC-99%, ramp the value between Max<->0 // When real SOC is between RAMPDOWN_SOC-99%, ramp the value between Max<->0
datalayer.battery.status.max_charge_power_W = datalayer.battery.status.max_charge_power_W =
MAX_CHARGE_POWER_ALLOWED_W * datalayer.battery.status.override_charge_power_W *
(1 - (datalayer.battery.status.real_soc - RAMPDOWN_SOC) / (10000.0 - RAMPDOWN_SOC)); (1 - (datalayer.battery.status.real_soc - RAMPDOWN_SOC) / (10000.0 - RAMPDOWN_SOC));
} else { // No limits, max charging power allowed } else { // No limits, max charging power allowed
datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_ALLOWED_W; datalayer.battery.status.max_charge_power_W = datalayer.battery.status.override_charge_power_W;
} }
// Discharge power is also set in .h file (TODO: Remove this estimation when real value has been found) // Discharge power is also set in .h file (TODO: Remove this estimation when real value has been found)
datalayer.battery.status.max_discharge_power_W = MAX_DISCHARGE_POWER_ALLOWED_W; datalayer.battery.status.max_discharge_power_W = datalayer.battery.status.override_discharge_power_W;
datalayer.battery.status.temperature_min_dC = temperature_lowest_C * 10; datalayer.battery.status.temperature_min_dC = temperature_lowest_C * 10;

View file

@ -16,8 +16,6 @@ class BoltAmperaBattery : public CanBattery {
private: private:
BoltAmperaHtmlRenderer renderer; BoltAmperaHtmlRenderer renderer;
static const int MAX_DISCHARGE_POWER_ALLOWED_W = 10000;
static const int MAX_CHARGE_POWER_ALLOWED_W = 10000;
static const int MAX_CHARGE_POWER_WHEN_TOPBALANCING_W = 500; static const int MAX_CHARGE_POWER_WHEN_TOPBALANCING_W = 500;
static const int RAMPDOWN_SOC = static const int RAMPDOWN_SOC =
9000; // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00% 9000; // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00%

View file

@ -20,9 +20,11 @@ void CellPowerBms::update_values() {
datalayer.battery.status.current_dA = battery_pack_current_dA; datalayer.battery.status.current_dA = battery_pack_current_dA;
datalayer.battery.status.max_charge_power_W = 5000; //TODO, is this available via CAN? datalayer.battery.status.max_charge_power_W =
datalayer.battery.status.override_charge_power_W; //TODO, is this available via CAN?
datalayer.battery.status.max_discharge_power_W = 5000; //TODO, is this available via CAN? datalayer.battery.status.max_discharge_power_W =
datalayer.battery.status.override_discharge_power_W; //TODO, is this available via CAN?
datalayer.battery.status.temperature_min_dC = (int16_t)(pack_temperature_low_C * 10); datalayer.battery.status.temperature_min_dC = (int16_t)(pack_temperature_low_C * 10);

View file

@ -20,9 +20,11 @@ void ImievCZeroIonBattery::
(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);
//We do not know the max charge/discharge power is sent by the battery. We hardcode value for now. //We do not know the max charge/discharge power is sent by the battery. We hardcode value for now.
datalayer.battery.status.max_charge_power_W = 10000; // 10kW //TODO: Fix when CAN is decoded datalayer.battery.status.max_charge_power_W =
datalayer.battery.status.override_charge_power_W; //TODO: Fix when CAN is decoded
datalayer.battery.status.max_discharge_power_W = 10000; // 10kW //TODO: Fix when CAN is decoded datalayer.battery.status.max_discharge_power_W =
datalayer.battery.status.override_discharge_power_W; //TODO: Fix when CAN is decoded
static int n = sizeof(cell_voltages) / sizeof(cell_voltages[0]); static int n = sizeof(cell_voltages) / sizeof(cell_voltages[0]);
max_volt_cel = cell_voltages[0]; // Initialize max with the first element of the array max_volt_cel = cell_voltages[0]; // Initialize max with the first element of the array

View file

@ -33,20 +33,20 @@ void RjxzsBms::update_values() {
datalayer.battery.status.current_dA = total_current; datalayer.battery.status.current_dA = total_current;
} }
// Charge power is set in .h file // Charge power is manually set
if (datalayer.battery.status.real_soc > 9900) { if (datalayer.battery.status.real_soc > 9900) {
datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_WHEN_TOPBALANCING_W; datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_WHEN_TOPBALANCING_W;
} else if (datalayer.battery.status.real_soc > RAMPDOWN_SOC) { } else if (datalayer.battery.status.real_soc > RAMPDOWN_SOC) {
// When real SOC is between RAMPDOWN_SOC-99%, ramp the value between Max<->0 // When real SOC is between RAMPDOWN_SOC-99%, ramp the value between Max<->0
datalayer.battery.status.max_charge_power_W = datalayer.battery.status.max_charge_power_W =
MAX_CHARGE_POWER_ALLOWED_W * datalayer.battery.status.override_charge_power_W *
(1 - (datalayer.battery.status.real_soc - RAMPDOWN_SOC) / (10000.0 - RAMPDOWN_SOC)); (1 - (datalayer.battery.status.real_soc - RAMPDOWN_SOC) / (10000.0 - RAMPDOWN_SOC));
} else { // No limits, max charging power allowed } else { // No limits, max charging power allowed
datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_ALLOWED_W; datalayer.battery.status.max_charge_power_W = datalayer.battery.status.override_charge_power_W;
} }
// Discharge power is also set in .h file // Discharge power is manually set
datalayer.battery.status.max_discharge_power_W = MAX_DISCHARGE_POWER_ALLOWED_W; datalayer.battery.status.max_discharge_power_W = datalayer.battery.status.override_discharge_power_W;
uint16_t temperatures[] = { uint16_t temperatures[] = {
module_1_temperature, module_2_temperature, module_3_temperature, module_4_temperature, module_1_temperature, module_2_temperature, module_3_temperature, module_4_temperature,

View file

@ -15,9 +15,6 @@ class RjxzsBms : public CanBattery {
static constexpr const char* Name = "RJXZS BMS, DIY battery"; static constexpr const char* Name = "RJXZS BMS, DIY battery";
private: private:
/* Tweak these according to your battery build */
static const int MAX_DISCHARGE_POWER_ALLOWED_W = 5000;
static const int MAX_CHARGE_POWER_ALLOWED_W = 5000;
static const int MAX_CHARGE_POWER_WHEN_TOPBALANCING_W = 500; static const int MAX_CHARGE_POWER_WHEN_TOPBALANCING_W = 500;
static const int RAMPDOWN_SOC = static const int RAMPDOWN_SOC =
9000; // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00% 9000; // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00%

View file

@ -627,8 +627,8 @@ void TeslaBattery::
// Define the allowed discharge power // Define the allowed discharge power
datalayer.battery.status.max_discharge_power_W = (battery_max_discharge_current * (battery_volts / 10)); datalayer.battery.status.max_discharge_power_W = (battery_max_discharge_current * (battery_volts / 10));
// Cap the allowed discharge power if higher than the maximum discharge power allowed // Cap the allowed discharge power if higher than the maximum discharge power allowed
if (datalayer.battery.status.max_discharge_power_W > MAXDISCHARGEPOWERALLOWED) { if (datalayer.battery.status.max_discharge_power_W > datalayer.battery.status.override_discharge_power_W) {
datalayer.battery.status.max_discharge_power_W = MAXDISCHARGEPOWERALLOWED; datalayer.battery.status.max_discharge_power_W = datalayer.battery.status.override_discharge_power_W;
} }
//The allowed charge power behaves strangely. We instead estimate this value //The allowed charge power behaves strangely. We instead estimate this value
@ -649,7 +649,7 @@ void TeslaBattery::
} }
} }
} else { // No limits, max charging power allowed } else { // No limits, max charging power allowed
datalayer.battery.status.max_charge_power_W = MAXCHARGEPOWERALLOWED; datalayer.battery.status.max_charge_power_W = datalayer.battery.status.override_charge_power_W;
} }
datalayer.battery.status.temperature_min_dC = battery_min_temp; datalayer.battery.status.temperature_min_dC = battery_min_temp;

View file

@ -39,15 +39,6 @@ class TeslaBattery : public CanBattery {
TeslaHtmlRenderer renderer; TeslaHtmlRenderer renderer;
protected: protected:
/* Modify these if needed */
static const int MAXCHARGEPOWERALLOWED =
15000; // 15000W we use a define since the value supplied by Tesla is always 0
static const int MAXDISCHARGEPOWERALLOWED =
60000; // 60000W we use a define since the value supplied by Tesla is always 0
// Set this to true to try to close contactors/full startup even with no inverter defined/connected
bool batteryTestOverride = false;
/* Do not change anything below this line! */ /* Do not change anything below this line! */
static const int RAMPDOWN_SOC = 900; // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00% static const int RAMPDOWN_SOC = 900; // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00%
static const int RAMPDOWNPOWERALLOWED = 10000; // What power we ramp down from towards top balancing static const int RAMPDOWNPOWERALLOWED = 10000; // What power we ramp down from towards top balancing

View file

@ -156,6 +156,10 @@ void init_stored_settings() {
datalayer.system.info.SD_logging_active = settings.getBool("SDLOGENABLED", false); datalayer.system.info.SD_logging_active = settings.getBool("SDLOGENABLED", false);
datalayer.battery.status.led_mode = (led_mode_enum)settings.getUInt("LEDMODE", false); datalayer.battery.status.led_mode = (led_mode_enum)settings.getUInt("LEDMODE", false);
//Some early integrations need manually set allowed charge/discharge power
datalayer.battery.status.override_charge_power_W = settings.getUInt("CHGPOWER", 1000);
datalayer.battery.status.override_discharge_power_W = settings.getUInt("DCHGPOWER", 1000);
// WIFI AP is enabled by default unless disabled in the settings // WIFI AP is enabled by default unless disabled in the settings
wifiap_enabled = settings.getBool("WIFIAPENABLED", true); wifiap_enabled = settings.getBool("WIFIAPENABLED", true);
wifi_channel = settings.getUInt("WIFICHANNEL", 2000); wifi_channel = settings.getUInt("WIFICHANNEL", 2000);

View file

@ -69,6 +69,13 @@ struct DATALAYER_BATTERY_STATUS_TYPE {
/** Instantaneous battery current in deciAmpere. 95 = 9.5 A */ /** Instantaneous battery current in deciAmpere. 95 = 9.5 A */
int16_t current_dA; int16_t current_dA;
/* Some early integrations do not support reading allowed charge power from battery
On these integrations we need to have the user specify what limits the battery can take */
/** Overriden allowed battery discharge power in Watts. Set by user */
uint32_t override_discharge_power_W = 0;
/** Overriden allowed battery charge power in Watts. Set by user */
uint32_t override_charge_power_W = 0;
/** uint16_t */ /** uint16_t */
/** State of health in integer-percent x 100. 9900 = 99.00% */ /** State of health in integer-percent x 100. 9900 = 99.00% */
uint16_t soh_pptt = 9900; uint16_t soh_pptt = 9900;

View file

@ -299,6 +299,14 @@ String settings_processor(const String& var, BatteryEmulatorSettingsStore& setti
return String(settings.getUInt("WIFICHANNEL", 0)); return String(settings.getUInt("WIFICHANNEL", 0));
} }
if (var == "CHGPOWER") {
return String(settings.getUInt("CHGPOWER", 0));
}
if (var == "DCHGPOWER") {
return String(settings.getUInt("DCHGPOWER", 0));
}
if (var == "LOCALIP1") { if (var == "LOCALIP1") {
return String(settings.getUInt("LOCALIP1", 0)); return String(settings.getUInt("LOCALIP1", 0));
} }
@ -869,6 +877,17 @@ const char* getCANInterfaceName(CAN_Interface interface) {
display: contents; display: contents;
} }
form .if-estimated { display: none; } /* Integrations with manually set charge/discharge power */
form[data-battery="3"] .if-estimated,
form[data-battery="4"] .if-estimated,
form[data-battery="6"] .if-estimated,
form[data-battery="14"] .if-estimated,
form[data-battery="24"] .if-estimated,
form[data-battery="32"] .if-estimated,
form[data-battery="33"] .if-estimated {
display: contents;
}
form .if-dblbtr { display: none; } form .if-dblbtr { display: none; }
form[data-dblbtr="true"] .if-dblbtr { form[data-dblbtr="true"] .if-dblbtr {
display: contents; display: contents;
@ -963,6 +982,14 @@ const char* getCANInterfaceName(CAN_Interface interface) {
</select> </select>
</div> </div>
<div class="if-estimated">
<label>Manual charging power, watt: </label>
<input name='CHGPOWER' pattern="^[0-9]+$" type='text' value='%CHGPOWER%' />
<label>Manual discharge power, watt: </label>
<input name='DCHGPOWER' pattern="^[0-9]+$" type='text' value='%DCHGPOWER%' />
</div>
<div class="if-battery"> <div class="if-battery">
<label for='BATTCOMM'>Battery comm I/F: </label><select name='BATTCOMM' id='BATTCOMM'> <label for='BATTCOMM'>Battery comm I/F: </label><select name='BATTCOMM' id='BATTCOMM'>
%BATTCOMM% %BATTCOMM%

View file

@ -467,6 +467,12 @@ void init_webserver() {
} else if (p->name() == "WIFICHANNEL") { } else if (p->name() == "WIFICHANNEL") {
auto type = atoi(p->value().c_str()); auto type = atoi(p->value().c_str());
settings.saveUInt("WIFICHANNEL", type); settings.saveUInt("WIFICHANNEL", type);
} else if (p->name() == "DCHGPOWER") {
auto type = atoi(p->value().c_str());
settings.saveUInt("DCHGPOWER", type);
} else if (p->name() == "CHGPOWER") {
auto type = atoi(p->value().c_str());
settings.saveUInt("CHGPOWER", type);
} else if (p->name() == "LOCALIP1") { } else if (p->name() == "LOCALIP1") {
auto type = atoi(p->value().c_str()); auto type = atoi(p->value().c_str());
settings.saveUInt("LOCALIP1", type); settings.saveUInt("LOCALIP1", type);