/*
;    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());
    }
  }
