OVMS3/OVMS.V3/components/vehicle/vehicle_bms.cpp

877 lines
28 KiB
C++

/*
; Project: Open Vehicle Monitor System
; Date: 14th March 2017
;
; Changes:
; 1.0 Initial release
;
; (C) 2011 Michael Stegen / Stegen Electronics
; (C) 2011-2017 Mark Webb-Johnson
; (C) 2011 Sonny Chen @ EPRO/DX
;
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
;
; The above copyright notice and this permission notice shall be included in
; all copies or substantial portions of the Software.
;
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
; THE SOFTWARE.
*/
#include "ovms_log.h"
static const char *TAG = "vehicle";
#include <stdio.h>
#include <algorithm>
#include <ovms_command.h>
#include <ovms_script.h>
#include <ovms_metrics.h>
#include <ovms_notify.h>
#include <metrics_standard.h>
#ifdef CONFIG_OVMS_COMP_WEBSERVER
#include <ovms_webserver.h>
#endif // #ifdef CONFIG_OVMS_COMP_WEBSERVER
#include <ovms_peripherals.h>
#include <string_writer.h>
#include "vehicle.h"
// Voltage stddev running average sample count:
#define VSTDDEV_SMOOTHCNT 5
void OvmsVehicle::BmsSetCellArrangementVoltage(int readings, int readingspermodule)
{
if (m_bms_voltages != NULL) delete m_bms_voltages;
m_bms_voltages = new float[readings];
if (m_bms_vmins != NULL) delete m_bms_vmins;
m_bms_vmins = new float[readings];
if (m_bms_vmaxs != NULL) delete m_bms_vmaxs;
m_bms_vmaxs = new float[readings];
if (m_bms_vdevmaxs != NULL) delete m_bms_vdevmaxs;
m_bms_vdevmaxs = new float[readings];
if (m_bms_valerts != NULL) delete m_bms_valerts;
m_bms_valerts = new OvmsStatus[readings];
m_bms_valerts_new = 0;
m_bms_bitset_v.clear();
m_bms_bitset_v.reserve(readings);
m_bms_readings_v = readings;
m_bms_readingspermodule_v = readingspermodule;
BmsResetCellVoltages(true);
}
// Internal entry for changing cell arrangements.
struct reading_entry_t
{
int cell_no;
float entry;
};
/**
* Changes the cell arrangement for Voltage, if needs be restoring any readings that were there.
* Really should only do work the first time it is called.. as for any one car, the number of readings
* should be consistent.
* @return true If the cell arrangement was changed.
*/
bool OvmsVehicle::BmsCheckChangeCellArrangementVoltage(int readings, int readingspermodule /*=0*/)
{
bool res = false;
if (readingspermodule <= 0)
{
// Passing in 0 will leave the readings per module unchanged.
readingspermodule = m_bms_readingspermodule_v;
}
if (readings != m_bms_readings_v)
{
res = true;
if (m_bms_bitset_cv == 0)
{
BmsSetCellArrangementVoltage(readings, readingspermodule);
}
else
{
// Store (potentially) sparse readings into a vector.
std::vector<reading_entry_t> cells;
// At most we will need the number of voltages set in the bitset.
cells.reserve(m_bms_bitset_cv);
reading_entry_t reading;
int maxv = readings;
if (m_bms_readings_v < maxv)
{
maxv = m_bms_readings_v;
}
for (int i = 0; i< maxv; ++i)
{
if (m_bms_bitset_v[i])
{
reading.cell_no = i;
reading.entry = m_bms_voltages[i];
cells.insert(cells.end(),reading);
}
}
BmsSetCellArrangementVoltage(readings, readingspermodule);
for ( std::vector<reading_entry_t>::iterator iter = cells.begin(); iter != cells.end(); ++iter)
{
BmsSetCellVoltage(iter->cell_no, iter->entry);
}
}
}
else if (readingspermodule != m_bms_readingspermodule_v)
{
// This only changes the read-out.
m_bms_readingspermodule_v = readingspermodule;
res = true;
}
return res;
}
/**
* Changes the cell arrangement for Temperature, if needs be restoring any readings that were there.
* Really should only do work the first time it is called.. as for any one car, the number of readings
* should be consistent.
* @return true If the cell arrangement was changed.
*/
bool OvmsVehicle::BmsCheckChangeCellArrangementTemperature(int readings, int readingspermodule /*=0*/)
{
bool res = false;
if (readingspermodule <= 0)
{
// Passing in 0 will leave the readings per module unchanged.
readingspermodule = m_bms_readingspermodule_t;
}
if (readings != m_bms_readings_t)
{
res = true;
if (m_bms_bitset_ct == 0)
{
BmsSetCellArrangementTemperature(readings, readingspermodule);
}
else
{
// Store (potentially) sparse readings into a vector.
std::vector<reading_entry_t> cells;
// At most we will need the number of Temperatures set in the bitset.
cells.reserve(m_bms_bitset_ct);
reading_entry_t reading;
int maxv = readings;
if (m_bms_readings_t < maxv)
{
maxv = m_bms_readings_t;
}
for (int i = 0; i< maxv; ++i)
{
if (m_bms_bitset_t[i])
{
reading.cell_no = i;
reading.entry = m_bms_temperatures[i];
cells.insert(cells.end(),reading);
}
}
BmsSetCellArrangementTemperature(readings, readingspermodule);
for ( std::vector<reading_entry_t>::iterator iter = cells.begin(); iter != cells.end(); ++iter)
{
BmsSetCellTemperature(iter->cell_no, iter->entry);
}
}
}
else if (readingspermodule != m_bms_readingspermodule_t)
{
// This only changes the read-out.
m_bms_readingspermodule_t = readingspermodule;
res = true;
}
return res;
}
void OvmsVehicle::BmsSetCellArrangementTemperature(int readings, int readingspermodule)
{
if (m_bms_temperatures != NULL) delete m_bms_temperatures;
m_bms_temperatures = new float[readings];
if (m_bms_tmins != NULL) delete m_bms_tmins;
m_bms_tmins = new float[readings];
if (m_bms_tmaxs != NULL) delete m_bms_tmaxs;
m_bms_tmaxs = new float[readings];
if (m_bms_tdevmaxs != NULL) delete m_bms_tdevmaxs;
m_bms_tdevmaxs = new float[readings];
if (m_bms_talerts != NULL) delete m_bms_talerts;
m_bms_talerts = new OvmsStatus[readings];
m_bms_talerts_new = 0;
m_bms_bitset_t.clear();
m_bms_bitset_t.reserve(readings);
m_bms_readings_t = readings;
m_bms_readingspermodule_t = readingspermodule;
BmsResetCellTemperatures(true);
}
int OvmsVehicle::BmsGetCellArangementVoltage(int* readings, int* readingspermodule)
{
if (readings) *readings = m_bms_readings_v;
if (readingspermodule) *readingspermodule = m_bms_readingspermodule_v;
return m_bms_readings_v;
}
int OvmsVehicle::BmsGetCellArangementTemperature(int* readings, int* readingspermodule)
{
if (readings) *readings = m_bms_readings_t;
if (readingspermodule) *readingspermodule = m_bms_readingspermodule_t;
return m_bms_readings_t;
}
void OvmsVehicle::BmsSetCellDefaultThresholdsVoltage(float warn, float alert,
float maxgrad /*=-1*/, float maxsddev /*=-1*/)
{
m_bms_defthr_vwarn = warn;
m_bms_defthr_valert = alert;
m_bms_defthr_vmaxgrad = (maxgrad < 0) ? BMS_DEFTHR_VMAXGRAD : maxgrad;
m_bms_defthr_vmaxsddev = (maxsddev < 0) ? BMS_DEFTHR_VMAXSDDEV : maxsddev;
}
void OvmsVehicle::BmsGetCellDefaultThresholdsVoltage(float* warn, float* alert,
float* maxgrad /*=NULL*/, float* maxsddev /*=NULL*/)
{
if (warn) *warn = m_bms_defthr_vwarn;
if (alert) *alert = m_bms_defthr_valert;
if (maxgrad) *maxgrad = m_bms_defthr_vmaxgrad;
if (maxsddev) *maxsddev = m_bms_defthr_vmaxsddev;
}
void OvmsVehicle::BmsSetCellDefaultThresholdsTemperature(float warn, float alert)
{
m_bms_defthr_twarn = warn;
m_bms_defthr_talert = alert;
}
void OvmsVehicle::BmsGetCellDefaultThresholdsTemperature(float* warn, float* alert)
{
if (warn) *warn = m_bms_defthr_twarn;
if (alert) *alert = m_bms_defthr_talert;
}
void OvmsVehicle::BmsSetCellLimitsVoltage(float min, float max)
{
m_bms_limit_vmin = min;
m_bms_limit_vmax = max;
}
void OvmsVehicle::BmsSetCellLimitsTemperature(float min, float max)
{
m_bms_limit_tmin = min;
m_bms_limit_tmax = max;
}
void OvmsVehicle::BmsSetCellVoltage(int index, float value)
{
// ESP_LOGV(TAG,"BmsSetCellVoltage(%d,%f) c=%d", index, value, m_bms_bitset_cv);
if ((index<0)||(index>=m_bms_readings_v)) return;
if ((value<m_bms_limit_vmin)||(value>m_bms_limit_vmax)) return;
m_bms_voltages[index] = value;
if (! m_bms_has_voltages)
{
m_bms_vmins[index] = value;
m_bms_vmaxs[index] = value;
}
else if (m_bms_vmins[index] > value)
m_bms_vmins[index] = value;
else if (m_bms_vmaxs[index] < value)
m_bms_vmaxs[index] = value;
if (m_bms_bitset_v[index] == false) m_bms_bitset_cv++;
if (m_bms_bitset_cv == m_bms_readings_v)
{
// Series complete, all cell voltages acquired
float thr_maxgrad = MyConfig.GetParamValueFloat("vehicle", "bms.dev.voltage.maxgrad", m_bms_defthr_vmaxgrad);
float thr_maxsddev = MyConfig.GetParamValueFloat("vehicle", "bms.dev.voltage.maxsddev", m_bms_defthr_vmaxsddev);
float thr_warn = MyConfig.GetParamValueFloat("vehicle", "bms.dev.voltage.warn", m_bms_defthr_vwarn);
float thr_alert = MyConfig.GetParamValueFloat("vehicle", "bms.dev.voltage.alert", m_bms_defthr_valert);
// Get min, max, avg & standard deviation:
double sum=0, sqrsum=0, avg, stddev=0;
float min=0, max=0;
for (int i=0; i<m_bms_readings_v; i++)
{
sum += m_bms_voltages[i];
sqrsum += SQR(m_bms_voltages[i]);
if (min==0 || m_bms_voltages[i]<min)
min = m_bms_voltages[i];
if (max==0 || m_bms_voltages[i]>max)
max = m_bms_voltages[i];
}
avg = sum / m_bms_readings_v;
stddev = sqrt(LIMIT_MIN((sqrsum / m_bms_readings_v) - SQR(avg), 0));
// Get gradient:
double sumn = 0, sumd = 0;
for (int i=0; i<m_bms_readings_v; i++)
{
sumn += (i - (m_bms_readings_v / 2 - 0.5)) * (m_bms_voltages[i] - avg);
sumd += SQR(i - (m_bms_readings_v / 2 - 0.5));
}
float grad = (sumn / sumd) * m_bms_readings_v;
// …publish to metrics:
StandardMetrics.ms_v_bat_pack_vmin->SetValue(min);
StandardMetrics.ms_v_bat_pack_vmax->SetValue(max);
StandardMetrics.ms_v_bat_pack_vavg->SetValue(ROUNDPREC(avg, 5));
StandardMetrics.ms_v_bat_pack_vstddev->SetValue(ROUNDPREC(stddev, 5));
StandardMetrics.ms_v_bat_pack_vgrad->SetValue(ROUNDPREC(grad, 5));
StandardMetrics.ms_v_bat_cell_voltage->SetElemValues(0, m_bms_readings_v, m_bms_voltages);
StandardMetrics.ms_v_bat_cell_vmin->SetElemValues(0, m_bms_readings_v, m_bms_vmins);
StandardMetrics.ms_v_bat_cell_vmax->SetElemValues(0, m_bms_readings_v, m_bms_vmaxs);
// Voltages are very volatile and may respond to a load change within the sensor query loop.
// To detect an inconsistent series, we check for a too high gradient and/or a too high
// offset of the momentary stddev level from the previously observed average:
bool series_valid;
if (ABS(grad) > thr_maxgrad)
{
series_valid = false;
}
else if (m_bms_vstddev_cnt < VSTDDEV_SMOOTHCNT)
{
// skip the first VSTDDEV_SMOOTHCNT series to init the average:
m_bms_vstddev_cnt++;
m_bms_vstddev_avg = ((m_bms_vstddev_cnt-1) * m_bms_vstddev_avg + stddev) / m_bms_vstddev_cnt;
series_valid = false;
}
else if (stddev - m_bms_vstddev_avg > thr_maxsddev)
{
series_valid = false;
}
else
{
m_bms_vstddev_avg = ((VSTDDEV_SMOOTHCNT-1) * m_bms_vstddev_avg + stddev) / VSTDDEV_SMOOTHCNT;
series_valid = true;
}
// Check cell deviations only if the series appears to be consistent:
if (series_valid)
{
float dev;
for (int i=0; i<m_bms_readings_v; i++)
{
dev = ROUNDPREC(m_bms_voltages[i] - avg, 5);
if (ABS(dev) > ABS(m_bms_vdevmaxs[i]))
m_bms_vdevmaxs[i] = dev;
if (ABS(dev) >= stddev + thr_alert && m_bms_valerts[i] <= OvmsStatus::Warn)
{
m_bms_valerts[i] = OvmsStatus::Alert;
m_bms_valerts_new++; // trigger notification
}
else if (ABS(dev) >= stddev + thr_warn && m_bms_valerts[i] < OvmsStatus::Warn)
m_bms_valerts[i] = OvmsStatus::Warn;
}
// Publish deviation maximums & alerts:
if (stddev > StandardMetrics.ms_v_bat_pack_vstddev_max->AsFloat())
StandardMetrics.ms_v_bat_pack_vstddev_max->SetValue(stddev);
StandardMetrics.ms_v_bat_cell_vdevmax->SetElemValues(0, m_bms_readings_v, m_bms_vdevmaxs);
StandardMetrics.ms_v_bat_cell_valert->SetElemValues(0, m_bms_readings_v, (short *)m_bms_valerts);
}
// complete:
m_bms_has_voltages = true;
m_bms_bitset_v.clear();
m_bms_bitset_v.resize(m_bms_readings_v);
m_bms_bitset_cv = 0;
}
else
{
m_bms_bitset_v[index] = true;
}
}
void OvmsVehicle::BmsSetCellTemperature(int index, float value)
{
// ESP_LOGV(TAG,"BmsSetCellTemperature(%d,%f) c=%d", index, value, m_bms_bitset_ct);
if ((index<0)||(index>=m_bms_readings_t)) return;
if ((value<m_bms_limit_tmin)||(value>m_bms_limit_tmax)) return;
m_bms_temperatures[index] = value;
if (! m_bms_has_temperatures)
{
m_bms_tmins[index] = value;
m_bms_tmaxs[index] = value;
}
else if (m_bms_tmins[index] > value)
m_bms_tmins[index] = value;
else if (m_bms_tmaxs[index] < value)
m_bms_tmaxs[index] = value;
if (m_bms_bitset_t[index] == false) m_bms_bitset_ct++;
if (m_bms_bitset_ct == m_bms_readings_t)
{
// Series complete, all cell temperatures acquired
float thr_warn = MyConfig.GetParamValueFloat("vehicle", "bms.dev.temp.warn", m_bms_defthr_twarn);
float thr_alert = MyConfig.GetParamValueFloat("vehicle", "bms.dev.temp.alert", m_bms_defthr_talert);
// get min, max, avg & standard deviation:
double sum=0, sqrsum=0, avg, stddev=0;
float min=0, max=0;
for (int i=0; i<m_bms_readings_t; i++)
{
sum += m_bms_temperatures[i];
sqrsum += SQR(m_bms_temperatures[i]);
if (min==0 || m_bms_temperatures[i]<min)
min = m_bms_temperatures[i];
if (max==0 || m_bms_temperatures[i]>max)
max = m_bms_temperatures[i];
}
avg = sum / m_bms_readings_t;
stddev = sqrt(LIMIT_MIN((sqrsum / m_bms_readings_t) - SQR(avg), 0));
// check cell deviations:
float dev;
for (int i=0; i<m_bms_readings_t; i++)
{
dev = ROUNDPREC(m_bms_temperatures[i] - avg, 2);
if (ABS(dev) > ABS(m_bms_tdevmaxs[i]))
m_bms_tdevmaxs[i] = dev;
if (ABS(dev) >= stddev + thr_alert && m_bms_talerts[i] < OvmsStatus::Alert)
{
m_bms_talerts[i] = OvmsStatus::Alert;
m_bms_talerts_new++; // trigger notification
}
else if (ABS(dev) >= stddev + thr_warn && m_bms_valerts[i] < OvmsStatus::Warn)
m_bms_talerts[i] = OvmsStatus::Warn;
}
// publish to metrics:
avg = ROUNDPREC(avg, 2);
stddev = ROUNDPREC(stddev, 2);
StandardMetrics.ms_v_bat_pack_tmin->SetValue(min);
StandardMetrics.ms_v_bat_pack_tmax->SetValue(max);
StandardMetrics.ms_v_bat_pack_tavg->SetValue(avg);
StandardMetrics.ms_v_bat_pack_tstddev->SetValue(stddev);
if (stddev > StandardMetrics.ms_v_bat_pack_tstddev_max->AsFloat())
StandardMetrics.ms_v_bat_pack_tstddev_max->SetValue(stddev);
StandardMetrics.ms_v_bat_cell_temp->SetElemValues(0, m_bms_readings_t, m_bms_temperatures);
StandardMetrics.ms_v_bat_cell_tmin->SetElemValues(0, m_bms_readings_t, m_bms_tmins);
StandardMetrics.ms_v_bat_cell_tmax->SetElemValues(0, m_bms_readings_t, m_bms_tmaxs);
StandardMetrics.ms_v_bat_cell_tdevmax->SetElemValues(0, m_bms_readings_t, m_bms_tdevmaxs);
StandardMetrics.ms_v_bat_cell_talert->SetElemValues(0, m_bms_readings_t, (short *) m_bms_talerts);
// complete:
m_bms_has_temperatures = true;
m_bms_bitset_t.clear();
m_bms_bitset_t.resize(m_bms_readings_t);
m_bms_bitset_ct = 0;
}
else
{
m_bms_bitset_t[index] = true;
}
}
void OvmsVehicle::BmsRestartCellVoltages()
{
m_bms_bitset_v.clear();
m_bms_bitset_v.resize(m_bms_readings_v);
m_bms_bitset_cv = 0;
}
void OvmsVehicle::BmsRestartCellTemperatures()
{
m_bms_bitset_t.clear();
m_bms_bitset_t.resize(m_bms_readings_t);
m_bms_bitset_ct = 0;
}
void OvmsVehicle::BmsResetCellVoltages(bool full /*=false*/)
{
if (m_bms_readings_v > 0)
{
m_bms_bitset_v.clear();
m_bms_bitset_v.resize(m_bms_readings_v);
m_bms_bitset_cv = 0;
m_bms_has_voltages = false;
for (int k=0; k<m_bms_readings_v; k++)
{
m_bms_vmins[k] = 0;
m_bms_vmaxs[k] = 0;
m_bms_vdevmaxs[k] = 0;
m_bms_valerts[k] = OvmsStatus::OK;
}
m_bms_valerts_new = 0;
m_bms_vstddev_cnt = 0;
m_bms_vstddev_avg = 0;
if (full) StandardMetrics.ms_v_bat_cell_voltage->ClearValue();
StandardMetrics.ms_v_bat_cell_vmin->ClearValue();
StandardMetrics.ms_v_bat_cell_vmax->ClearValue();
StandardMetrics.ms_v_bat_cell_vdevmax->ClearValue();
StandardMetrics.ms_v_bat_cell_valert->ClearValue();
StandardMetrics.ms_v_bat_pack_vstddev_max->SetValue(StandardMetrics.ms_v_bat_pack_vstddev->AsFloat());
}
}
void OvmsVehicle::BmsResetCellTemperatures(bool full /*=false*/)
{
if (m_bms_readings_t > 0)
{
m_bms_bitset_t.clear();
m_bms_bitset_t.resize(m_bms_readings_t);
m_bms_bitset_ct = 0;
m_bms_has_temperatures = false;
for (int k=0; k<m_bms_readings_t; k++)
{
m_bms_tmins[k] = 0;
m_bms_tmaxs[k] = 0;
m_bms_tdevmaxs[k] = 0;
m_bms_talerts[k] = OvmsStatus::OK;
}
m_bms_talerts_new = 0;
if (full) StandardMetrics.ms_v_bat_cell_temp->ClearValue();
StandardMetrics.ms_v_bat_cell_tmin->ClearValue();
StandardMetrics.ms_v_bat_cell_tmax->ClearValue();
StandardMetrics.ms_v_bat_cell_tdevmax->ClearValue();
StandardMetrics.ms_v_bat_cell_talert->ClearValue();
StandardMetrics.ms_v_bat_pack_tstddev_max->SetValue(StandardMetrics.ms_v_bat_pack_tstddev->AsFloat());
}
}
void OvmsVehicle::BmsResetCellStats()
{
BmsResetCellVoltages(false);
BmsResetCellTemperatures(false);
}
template<typename INT>
INT round_up_div(INT value, INT divis)
{
return (value + divis -1) / divis;
}
void OvmsVehicle::BmsStatus(int verbosity, OvmsWriter* writer, vehicle_bms_status_t statusmode)
{
auto check_max_cols = [](int total_cols, int maximum)
{
if (maximum <= 1)
return 1;
if (total_cols <= maximum)
return total_cols;
if (maximum >= 4 && total_cols % 4 == 0)
return 4;
if (total_cols % 3 == 0)
return 3;
if (maximum >= 5 && total_cols % 5 == 0)
return 5;
return maximum;
};
int c;
bool show_voltage = m_bms_has_voltages;
bool show_temperature = m_bms_has_temperatures;
switch (statusmode)
{
case vehicle_bms_status_t::Both:
break;
case vehicle_bms_status_t::Voltage:
show_temperature = false;
break;
case vehicle_bms_status_t::Temperature:
show_voltage = false;
break;
}
if ((!show_voltage) && (!show_temperature))
{
const char *datatype= "status";
switch (statusmode)
{
case vehicle_bms_status_t::Both:
break;
case vehicle_bms_status_t::Voltage:
datatype = "voltage";
break;
case vehicle_bms_status_t::Temperature:
datatype = "temperature";
break;
}
writer->printf("No BMS %s data available\n", datatype);
return;
}
int vwarn=0, valert=0;
int twarn=0, talert=0;
for (c=0; c<m_bms_readings_v; c++)
{
switch (m_bms_valerts[c])
{
case OvmsStatus::OK: break;
case OvmsStatus::Warn: vwarn++; break;
case OvmsStatus::Alert: valert++;break;
}
}
for (c=0; c<m_bms_readings_t; c++)
{
switch (m_bms_talerts[c])
{
case OvmsStatus::OK: break;
case OvmsStatus::Warn: twarn++; break;
case OvmsStatus::Alert: talert++; break;
}
}
if (show_voltage)
{
writer->puts("Voltage:");
writer->printf(" Average: %5.3fV [%5.3fV - %5.3fV]\n",
StdMetrics.ms_v_bat_pack_vavg->AsFloat(),
StdMetrics.ms_v_bat_pack_vmin->AsFloat(),
StdMetrics.ms_v_bat_pack_vmax->AsFloat());
writer->printf(" Deviation: SD %6.2fmV [max %.2fmV], %d warnings, %d alerts\n",
StdMetrics.ms_v_bat_pack_vstddev->AsFloat()*1000,
StdMetrics.ms_v_bat_pack_vstddev_max->AsFloat()*1000,
vwarn, valert);
}
if (show_temperature)
{
writer->puts("Temperature:");
writer->printf(" Average: %5.1fC [%5.1fC - %5.1fC]\n",
StdMetrics.ms_v_bat_pack_tavg->AsFloat(),
StdMetrics.ms_v_bat_pack_tmin->AsFloat(),
StdMetrics.ms_v_bat_pack_tmax->AsFloat());
writer->printf(" Deviation: SD %6.2fC [max %.2fC], %d warnings, %d alerts\n",
StdMetrics.ms_v_bat_pack_tstddev->AsFloat(),
StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat(),
twarn, talert);
}
writer->puts("Cells:");
int kv = 0;
int kt = 0;
int module_count = 0;
if (show_voltage)
module_count = round_up_div(m_bms_readings_v,m_bms_readingspermodule_v);
if (show_temperature)
{
int temp_module_count = round_up_div(m_bms_readings_t,m_bms_readingspermodule_t);
if (temp_module_count > module_count)
module_count = temp_module_count;
}
int max_cols_v = 0;
if (show_voltage)
max_cols_v = check_max_cols(m_bms_readingspermodule_v, show_temperature?4:5);
int max_cols_t = 0;
if (show_temperature)
max_cols_t = check_max_cols(m_bms_readingspermodule_t, 5-max_cols_v);
for (int module = 0; module < module_count; ++module)
{
writer->printf(" +");
if (show_voltage)
{
for (c=0;c<max_cols_v;c++) { writer->printf("-------"); }
writer->printf("-+");
}
if (show_temperature) {
for (c=0;c<max_cols_t;c++) { writer->printf("-------"); }
writer->printf("-+");
}
writer->puts("");
int rows_v = 0, rows_t = 0;
int reading_left_v = 0, reading_left_t = 0;
if (show_voltage)
{
int items_left_v = m_bms_readings_v - kv;
reading_left_v = std::min(items_left_v, m_bms_readingspermodule_v);
rows_v = round_up_div(reading_left_v, max_cols_v);
}
if (show_temperature)
{
int items_left_t = m_bms_readings_t - kt;
reading_left_t = std::min(items_left_t, m_bms_readingspermodule_t);
rows_t = round_up_div(reading_left_t, max_cols_t);
}
int rows = std::max(rows_v,rows_t);
for (int row = 0 ; row < rows; ++row)
{
if (row == 0)
writer->printf("%3d |",module+1);
else
writer->printf(" |");
if (show_voltage)
{
for (c=0; c<max_cols_v; c++)
{
if (kv < m_bms_readings_v && (reading_left_v > 0))
{
writer->printf(" %5.3fV",m_bms_voltages[kv]);
--reading_left_v;
++kv;
}
else
writer->printf(" ");
}
writer->printf(" |");
}
if (show_temperature)
{
for (c=0; c<m_bms_readingspermodule_t; c++)
{
if (kt < m_bms_readings_t && (reading_left_t > 0))
{
writer->printf(" %5.1fC",m_bms_temperatures[kt]);
--reading_left_t;
++kt;
}
else
writer->printf(" ");
}
writer->printf(" |");
}
writer->puts("");
}
}
writer->printf(" +");
if (show_voltage)
{
for (c=0;c<max_cols_v;c++) { writer->printf("-------"); }
writer->printf("-+");
}
if (show_temperature)
{
for (c=0;c<max_cols_t;c++) { writer->printf("-------"); }
writer->printf("-+");
}
writer->puts("");
}
bool OvmsVehicle::FormatBmsAlerts(int verbosity, OvmsWriter* writer, bool show_warnings)
{
int has_valerts = 0, has_talerts = 0;
bool verbose = (verbosity > COMMAND_RESULT_SMS);
writer->puts("Battery cell deviation:");
// Voltages:
writer->printf("Voltage: StdDev %dmV", (int)(StdMetrics.ms_v_bat_pack_vstddev_max->AsFloat() * 1000));
for (int i=0; i<m_bms_readings_v; i++)
{
OvmsStatus sts = OvmsStatus(StdMetrics.ms_v_bat_cell_valert->GetElemValue(i));
switch (sts)
{
case OvmsStatus::OK: continue;
case OvmsStatus::Warn: if (!show_warnings) continue;
case OvmsStatus::Alert: ;
}
has_valerts++;
if (verbose || has_valerts <= 5)
{
int dev = StdMetrics.ms_v_bat_cell_vdevmax->GetElemValue(i) * 1000;
writer->printf("\n %c #%02d: %+4dmV", (sts==OvmsStatus::Warn) ? '?' : '!', i+1, dev);
}
else
{
writer->printf("\n [...]");
break;
}
}
writer->printf("%s\n", has_valerts ? "" : ", cells OK");
// Temperatures:
// (Note: '°' is not SMS safe, so we only output 'C')
writer->printf("Temperature: StdDev %.1fC", StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat());
for (int i=0; i<m_bms_readings_v; i++)
{
OvmsStatus sts = OvmsStatus(StdMetrics.ms_v_bat_cell_talert->GetElemValue(i));
switch (sts)
{
case OvmsStatus::OK: continue;
case OvmsStatus::Warn: if (!show_warnings) continue;
case OvmsStatus::Alert: ;
}
has_talerts++;
if (verbose || has_talerts <= 5)
{
float dev = StdMetrics.ms_v_bat_cell_tdevmax->GetElemValue(i);
writer->printf("\n %c #%02d: %+3.1fC", (sts==OvmsStatus::Warn) ? '?' : '!', i+1, dev);
}
else
{
writer->printf("\n [...]");
break;
}
}
writer->printf("%s\n", has_talerts ? "" : ", cells OK");
if (verbose || has_valerts >= 5 || has_talerts >= 5)
{
writer->puts("Details: bms status");
}
return (has_valerts > 0) || (has_talerts > 0);
}
void OvmsVehicle::NotifyBmsAlerts()
{
StringWriter buf(200);
if (FormatBmsAlerts(COMMAND_RESULT_SMS, &buf, false))
MyNotify.NotifyString("alert", "batt.bms.alert", buf.c_str());
}
void OvmsVehicle::BmsTicker()
{
// Alerts:
if (m_bms_valerts_new || m_bms_talerts_new)
{
ESP_LOGW(TAG, "BMS new alerts: %d voltages, %d temperatures", m_bms_valerts_new, m_bms_talerts_new);
MyEvents.SignalEvent("vehicle.alert.bms", NULL);
if (m_autonotifications && MyConfig.GetParamValueBool("vehicle", "bms.alerts.enabled", true))
NotifyBmsAlerts();
m_bms_valerts_new = 0;
m_bms_talerts_new = 0;
}
// Log cell voltages:
int vlog_interval = MyConfig.GetParamValueInt("vehicle", "bms.log.voltage.interval", 0);
if (vlog_interval > 0 && m_bms_vlog_last + vlog_interval < monotonictime &&
StdMetrics.ms_v_bat_cell_voltage->LastModified() > m_bms_vlog_last)
{
m_bms_vlog_last = monotonictime;
ESP_LOGI(TAG, "BMS cell voltage: avg=%.3f min=%.3f max=%.3f grad=%.1f stddev=%.1f sdavg=%.1f sdmax=%.1f cells: %s",
StdMetrics.ms_v_bat_pack_vavg->AsFloat(),
StdMetrics.ms_v_bat_pack_vmin->AsFloat(),
StdMetrics.ms_v_bat_pack_vmax->AsFloat(),
StdMetrics.ms_v_bat_pack_vgrad->AsFloat() * 1000,
StdMetrics.ms_v_bat_pack_vstddev->AsFloat() * 1000,
m_bms_vstddev_avg * 1000,
StdMetrics.ms_v_bat_pack_vstddev_max->AsFloat() * 1000,
StdMetrics.ms_v_bat_cell_voltage->AsString("", Native, 3).c_str());
}
// Log cell temperatures:
int tlog_interval = MyConfig.GetParamValueInt("vehicle", "bms.log.temp.interval", 0);
if (tlog_interval > 0 && m_bms_tlog_last + tlog_interval < monotonictime &&
StdMetrics.ms_v_bat_cell_temp->LastModified() > m_bms_tlog_last)
{
m_bms_tlog_last = monotonictime;
ESP_LOGI(TAG, "BMS cell temperature: avg=%.1f min=%.1f max=%.1f stddev=%.1f sdmax=%.1f cells: %s",
StdMetrics.ms_v_bat_pack_tavg->AsFloat(),
StdMetrics.ms_v_bat_pack_tmin->AsFloat(),
StdMetrics.ms_v_bat_pack_tmax->AsFloat(),
StdMetrics.ms_v_bat_pack_tstddev->AsFloat(),
StdMetrics.ms_v_bat_pack_tstddev_max->AsFloat(),
StdMetrics.ms_v_bat_cell_temp->AsString("", Native, 1).c_str());
}
}