925 lines
30 KiB
C++
925 lines
30 KiB
C++
|
/*
|
||
|
; Project: Open Vehicle Monitor System
|
||
|
; Subproject: Integration of support for the VW e-UP
|
||
|
;
|
||
|
; (c) 2021 sharkcow <sharkcow@gmx.de>, Chris van der Meijden, SokoFromNZ, Michael Balzer <dexter@dexters-web.de>
|
||
|
;
|
||
|
; Biggest thanks to Dimitrie78, E-Imo and 'der kleine Nik'.
|
||
|
;
|
||
|
; 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"
|
||
|
#include <string>
|
||
|
static const char *TAG = "v-vweup";
|
||
|
|
||
|
#define VERSION "0.22.4"
|
||
|
|
||
|
#include <stdio.h>
|
||
|
#include <string>
|
||
|
#include <iomanip>
|
||
|
#include "pcp.h"
|
||
|
#include "ovms_metrics.h"
|
||
|
#include "ovms_events.h"
|
||
|
#include "ovms_config.h"
|
||
|
#include "ovms_command.h"
|
||
|
#include "metrics_standard.h"
|
||
|
#include "ovms_notify.h"
|
||
|
|
||
|
#include "vehicle_vweup.h"
|
||
|
|
||
|
using namespace std;
|
||
|
|
||
|
/**
|
||
|
* Framework registration
|
||
|
*/
|
||
|
|
||
|
class OvmsVehicleVWeUpInit
|
||
|
{
|
||
|
public:
|
||
|
OvmsVehicleVWeUpInit();
|
||
|
} OvmsVehicleVWeUpInit __attribute__((init_priority(9000)));
|
||
|
|
||
|
OvmsVehicleVWeUpInit::OvmsVehicleVWeUpInit()
|
||
|
{
|
||
|
ESP_LOGI(TAG, "Registering Vehicle: VW e-Up (9000)");
|
||
|
MyVehicleFactory.RegisterVehicle<OvmsVehicleVWeUp>("VWUP", "VW e-Up");
|
||
|
}
|
||
|
|
||
|
|
||
|
OvmsVehicleVWeUp *OvmsVehicleVWeUp::GetInstance(OvmsWriter *writer)
|
||
|
{
|
||
|
OvmsVehicleVWeUp *eup = (OvmsVehicleVWeUp *) MyVehicleFactory.ActiveVehicle();
|
||
|
|
||
|
string type = StdMetrics.ms_v_type->AsString();
|
||
|
if (!eup || type != "VWUP") {
|
||
|
if (writer) {
|
||
|
writer->puts("Error: VW e-Up vehicle module not selected");
|
||
|
}
|
||
|
return NULL;
|
||
|
}
|
||
|
return eup;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Constructor & destructor
|
||
|
*/
|
||
|
|
||
|
//size_t OvmsVehicleVWeUp::m_modifier = 0;
|
||
|
|
||
|
OvmsVehicleVWeUp::OvmsVehicleVWeUp()
|
||
|
{
|
||
|
ESP_LOGI(TAG, "Start VW e-Up vehicle module");
|
||
|
|
||
|
// Init general state:
|
||
|
vweup_enable_write = false;
|
||
|
vweup_enable_obd = false;
|
||
|
vweup_enable_t26 = false;
|
||
|
vweup_con = 0;
|
||
|
vweup_modelyear = 0;
|
||
|
|
||
|
m_use_phase = UP_None;
|
||
|
m_obd_state = OBDS_Init;
|
||
|
|
||
|
m_soc_norm_start = 0;
|
||
|
m_soc_abs_start = 0;
|
||
|
m_energy_recd_start = 0;
|
||
|
m_energy_used_start = 0;
|
||
|
m_energy_charged_start = 0;
|
||
|
m_coulomb_recd_start = 0;
|
||
|
m_coulomb_used_start = 0;
|
||
|
m_coulomb_charged_start = 0;
|
||
|
m_charge_kwh_grid_start = 0;
|
||
|
m_charge_kwh_grid = 0;
|
||
|
|
||
|
m_chargestart_ticker = 0;
|
||
|
m_chargestop_ticker = 0;
|
||
|
m_chargestate_lastsoc = 100;
|
||
|
m_timermode_ticker = 0;
|
||
|
m_timermode_new = false;
|
||
|
|
||
|
m_chg_ctp_car = -1;
|
||
|
|
||
|
m_odo_trip = 0;
|
||
|
m_tripfrac_reftime = 0;
|
||
|
m_tripfrac_refspeed = 0;
|
||
|
|
||
|
// Init metrics:
|
||
|
m_version = MyMetrics.InitString("xvu.m.version", 0, VERSION " " __DATE__ " " __TIME__);
|
||
|
|
||
|
// init configs:
|
||
|
MyConfig.RegisterParam("xvu", "VW e-Up", true, true);
|
||
|
|
||
|
// Init commands:
|
||
|
OvmsCommand *cmd;
|
||
|
cmd_xvu = MyCommandApp.RegisterCommand("xvu", "VW e-Up");
|
||
|
|
||
|
cmd = cmd_xvu->RegisterCommand("polling", "OBD2 poller control");
|
||
|
cmd->RegisterCommand("status", "Get current polling status", ShellPollControl);
|
||
|
cmd->RegisterCommand("pause", "Pause polling if running", ShellPollControl);
|
||
|
cmd->RegisterCommand("continue", "Continue polling if paused", ShellPollControl);
|
||
|
|
||
|
// Load initial config:
|
||
|
ConfigChanged(NULL);
|
||
|
|
||
|
#ifdef CONFIG_OVMS_COMP_WEBSERVER
|
||
|
WebInit();
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
OvmsVehicleVWeUp::~OvmsVehicleVWeUp()
|
||
|
{
|
||
|
ESP_LOGI(TAG, "Stop VW e-Up vehicle module");
|
||
|
|
||
|
#ifdef CONFIG_OVMS_COMP_WEBSERVER
|
||
|
WebDeInit();
|
||
|
#endif
|
||
|
|
||
|
// delete metrics:
|
||
|
MyMetrics.DeregisterMetric(m_version);
|
||
|
// TODO: delete all xvu metrics
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
const char* OvmsVehicleVWeUp::VehicleShortName()
|
||
|
{
|
||
|
return "e-Up";
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
bool OvmsVehicleVWeUp::SetFeature(int key, const char *value)
|
||
|
{
|
||
|
int i;
|
||
|
int n;
|
||
|
switch (key) {
|
||
|
case 15: {
|
||
|
int bits = atoi(value);
|
||
|
MyConfig.SetParamValueBool("xvu", "canwrite", (bits & 1) != 0);
|
||
|
return true;
|
||
|
}
|
||
|
case 20:
|
||
|
// check:
|
||
|
if (strlen(value) == 0) {
|
||
|
value = "2020";
|
||
|
}
|
||
|
for (i = 0; i < strlen(value); i++) {
|
||
|
if (isdigit(value[i]) == false) {
|
||
|
value = "2020";
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
n = atoi(value);
|
||
|
if (n < 2013) {
|
||
|
value = "2013";
|
||
|
}
|
||
|
MyConfig.SetParamValue("xvu", "modelyear", value);
|
||
|
return true;
|
||
|
case 21:
|
||
|
// check:
|
||
|
if (strlen(value) == 0) {
|
||
|
value = "22";
|
||
|
}
|
||
|
for (i = 0; i < strlen(value); i++) {
|
||
|
if (isdigit(value[i]) == false) {
|
||
|
value = "22";
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
n = atoi(value);
|
||
|
if (n < 15) {
|
||
|
value = "15";
|
||
|
}
|
||
|
if (n > 30) {
|
||
|
value = "30";
|
||
|
}
|
||
|
MyConfig.SetParamValue("xvu", "cc_temp", value);
|
||
|
return true;
|
||
|
default:
|
||
|
return OvmsVehicle::SetFeature(key, value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const std::string OvmsVehicleVWeUp::GetFeature(int key)
|
||
|
{
|
||
|
switch (key) {
|
||
|
case 15: {
|
||
|
int bits = (MyConfig.GetParamValueBool("xvu", "canwrite", false) ? 1 : 0);
|
||
|
char buf[4];
|
||
|
sprintf(buf, "%d", bits);
|
||
|
return std::string(buf);
|
||
|
}
|
||
|
case 20:
|
||
|
return MyConfig.GetParamValue("xvu", "modelyear", STR(DEFAULT_MODEL_YEAR));
|
||
|
case 21:
|
||
|
return MyConfig.GetParamValue("xvu", "cc_temp", STR(21));
|
||
|
default:
|
||
|
return OvmsVehicle::GetFeature(key);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void OvmsVehicleVWeUp::ConfigChanged(OvmsConfigParam *param)
|
||
|
{
|
||
|
if (param && param->GetName() != "xvu") {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ESP_LOGD(TAG, "VW e-Up reload configuration");
|
||
|
int vweup_modelyear_new = MyConfig.GetParamValueInt("xvu", "modelyear", DEFAULT_MODEL_YEAR);
|
||
|
bool vweup_enable_obd_new = MyConfig.GetParamValueBool("xvu", "con_obd", true);
|
||
|
bool vweup_enable_t26_new = MyConfig.GetParamValueBool("xvu", "con_t26", true);
|
||
|
vweup_enable_write = MyConfig.GetParamValueBool("xvu", "canwrite", false);
|
||
|
vweup_cc_temp_int = MyConfig.GetParamValueInt("xvu", "cc_temp", 22);
|
||
|
int dc_interval = MyConfig.GetParamValueInt("xvu", "dc_interval", 0);
|
||
|
int cell_interval_drv = MyConfig.GetParamValueInt("xvu", "cell_interval_drv", 15);
|
||
|
int cell_interval_chg = MyConfig.GetParamValueInt("xvu", "cell_interval_chg", 60);
|
||
|
int cell_interval_awk = MyConfig.GetParamValueInt("xvu", "cell_interval_awk", 60);
|
||
|
|
||
|
bool do_obd_init = (
|
||
|
(!vweup_enable_obd && vweup_enable_obd_new) ||
|
||
|
(vweup_enable_t26_new != vweup_enable_t26) ||
|
||
|
(vweup_modelyear < 2020 && vweup_modelyear_new > 2019) ||
|
||
|
(vweup_modelyear_new < 2020 && vweup_modelyear > 2019) ||
|
||
|
(dc_interval != m_cfg_dc_interval) ||
|
||
|
(cell_interval_drv != m_cfg_cell_interval_drv) ||
|
||
|
(cell_interval_chg != m_cfg_cell_interval_chg) ||
|
||
|
(cell_interval_awk != m_cfg_cell_interval_awk));
|
||
|
|
||
|
vweup_modelyear = vweup_modelyear_new;
|
||
|
vweup_enable_obd = vweup_enable_obd_new;
|
||
|
vweup_enable_t26 = vweup_enable_t26_new;
|
||
|
m_cfg_dc_interval = dc_interval;
|
||
|
m_cfg_cell_interval_drv = cell_interval_drv;
|
||
|
m_cfg_cell_interval_chg = cell_interval_chg;
|
||
|
m_cfg_cell_interval_awk = cell_interval_awk;
|
||
|
|
||
|
// Connectors:
|
||
|
vweup_con = vweup_enable_obd * 2 + vweup_enable_t26;
|
||
|
if (!vweup_con) {
|
||
|
ESP_LOGW(TAG, "Module will not work without any connection!");
|
||
|
}
|
||
|
|
||
|
// Set model specific general vehicle properties:
|
||
|
// Note: currently using standard specs
|
||
|
// TODO: get actual capacity/SOH & max charge current
|
||
|
float socfactor = StdMetrics.ms_v_bat_soc->AsFloat() / 100;
|
||
|
float sohfactor = StdMetrics.ms_v_bat_soh->AsFloat() / 100;
|
||
|
if (sohfactor == 0) sohfactor = 1;
|
||
|
if (vweup_modelyear > 2019)
|
||
|
{
|
||
|
// 32.3 kWh net / 36.8 kWh gross, 2P84S = 120 Ah, 260 km WLTP
|
||
|
if (StdMetrics.ms_v_bat_cac->AsFloat() == 0)
|
||
|
StdMetrics.ms_v_bat_cac->SetValue(120 * sohfactor);
|
||
|
StdMetrics.ms_v_bat_range_full->SetValue(260 * sohfactor);
|
||
|
if (StdMetrics.ms_v_bat_range_ideal->AsFloat() == 0)
|
||
|
StdMetrics.ms_v_bat_range_ideal->SetValue(260 * sohfactor * socfactor);
|
||
|
if (StdMetrics.ms_v_bat_range_est->AsFloat() > 10 && StdMetrics.ms_v_bat_soc->AsFloat() > 10)
|
||
|
m_range_est_factor = StdMetrics.ms_v_bat_range_est->AsFloat() / StdMetrics.ms_v_bat_soc->AsFloat();
|
||
|
else
|
||
|
m_range_est_factor = 2.6f;
|
||
|
StdMetrics.ms_v_charge_climit->SetValue(32);
|
||
|
|
||
|
// Battery pack layout: 2P84S in 14 modules
|
||
|
BmsSetCellArrangementVoltage(84, 6);
|
||
|
BmsSetCellArrangementTemperature(14, 1);
|
||
|
BmsSetCellLimitsVoltage(2.0, 5.0);
|
||
|
BmsSetCellLimitsTemperature(-39, 200);
|
||
|
BmsSetCellDefaultThresholdsVoltage(0.020, 0.030);
|
||
|
BmsSetCellDefaultThresholdsTemperature(2.0, 3.0);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// 16.4 kWh net / 18.7 kWh gross, 2P102S = 50 Ah, 160 km WLTP
|
||
|
if (StdMetrics.ms_v_bat_cac->AsFloat() == 0)
|
||
|
StdMetrics.ms_v_bat_cac->SetValue(50 * sohfactor);
|
||
|
StdMetrics.ms_v_bat_range_full->SetValue(160 * sohfactor);
|
||
|
if (StdMetrics.ms_v_bat_range_ideal->AsFloat() == 0)
|
||
|
StdMetrics.ms_v_bat_range_ideal->SetValue(160 * sohfactor * socfactor);
|
||
|
if (StdMetrics.ms_v_bat_range_est->AsFloat() > 10 && StdMetrics.ms_v_bat_soc->AsFloat() > 10)
|
||
|
m_range_est_factor = StdMetrics.ms_v_bat_range_est->AsFloat() / StdMetrics.ms_v_bat_soc->AsFloat();
|
||
|
else
|
||
|
m_range_est_factor = 1.6f;
|
||
|
StdMetrics.ms_v_charge_climit->SetValue(16);
|
||
|
|
||
|
// Battery pack layout: 2P102S in 17 modules
|
||
|
BmsSetCellArrangementVoltage(102, 6);
|
||
|
BmsSetCellArrangementTemperature(17, 1);
|
||
|
BmsSetCellLimitsVoltage(2.0, 5.0);
|
||
|
BmsSetCellLimitsTemperature(-39, 200);
|
||
|
BmsSetCellDefaultThresholdsVoltage(0.020, 0.030);
|
||
|
BmsSetCellDefaultThresholdsTemperature(2.0, 3.0);
|
||
|
}
|
||
|
|
||
|
// Init OBD subsystem:
|
||
|
// (needs to be initialized first as the T26 module depends on OBD being ready)
|
||
|
if (vweup_enable_obd && do_obd_init) {
|
||
|
OBDInit();
|
||
|
}
|
||
|
else if (!vweup_enable_obd) {
|
||
|
OBDDeInit();
|
||
|
}
|
||
|
|
||
|
// Init T26 subsystem:
|
||
|
if (vweup_enable_t26) {
|
||
|
T26Init();
|
||
|
}
|
||
|
|
||
|
#ifdef CONFIG_OVMS_COMP_WEBSERVER
|
||
|
// Init Web subsystem:
|
||
|
WebDeInit(); // this can probably be done more elegantly... :-/
|
||
|
WebInit();
|
||
|
#endif
|
||
|
|
||
|
// Set standard SOH from configured source:
|
||
|
if (IsOBDReady())
|
||
|
{
|
||
|
std::string soh_source = MyConfig.GetParamValue("xvu", "bat.soh.source", "charge");
|
||
|
if (soh_source == "range" && m_bat_soh_range->IsDefined())
|
||
|
SetSOH(m_bat_soh_range->AsFloat());
|
||
|
else if (soh_source == "charge" && m_bat_soh_charge->IsDefined())
|
||
|
SetSOH(m_bat_soh_charge->AsFloat());
|
||
|
}
|
||
|
|
||
|
// Update charge time predictions:
|
||
|
UpdateChargeTimes();
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* MetricModified: hook into general listener for metrics changes
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::MetricModified(OvmsMetric* metric)
|
||
|
{
|
||
|
// If one of our SOH sources got updated, derive standard SOH, CAC and ranges from it
|
||
|
// if it's the configured SOH source:
|
||
|
if (metric == m_bat_soh_range || metric == m_bat_soh_charge)
|
||
|
{
|
||
|
// Check SOH source configuration:
|
||
|
float soh_new = 0;
|
||
|
std::string soh_source = MyConfig.GetParamValue("xvu", "bat.soh.source", "charge");
|
||
|
if (metric == m_bat_soh_range && soh_source == "range")
|
||
|
soh_new = metric->AsFloat();
|
||
|
else if (metric == m_bat_soh_charge && soh_source == "charge")
|
||
|
soh_new = metric->AsFloat();
|
||
|
|
||
|
// Update metrics:
|
||
|
if (soh_new)
|
||
|
SetSOH(soh_new);
|
||
|
}
|
||
|
|
||
|
// Pass update on to standard handler:
|
||
|
OvmsVehicle::MetricModified(metric);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* SetSOH: set SOH, derive standard SOH, CAC and ranges
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::SetSOH(float soh_new)
|
||
|
{
|
||
|
float soh_fct = soh_new / 100;
|
||
|
float cap_ah = soh_fct * ((vweup_modelyear > 2019) ? 120.0f : 50.0f);
|
||
|
float range_full = soh_fct * ((vweup_modelyear > 2019) ? 260.0f : 160.0f);
|
||
|
float soc_fct = StdMetrics.ms_v_bat_soc->AsFloat() / 100;
|
||
|
StdMetrics.ms_v_bat_soh->SetValue(soh_new);
|
||
|
StdMetrics.ms_v_bat_cac->SetValue(cap_ah);
|
||
|
StdMetrics.ms_v_bat_range_full->SetValue(range_full);
|
||
|
StdMetrics.ms_v_bat_range_ideal->SetValue(range_full * soc_fct);
|
||
|
}
|
||
|
|
||
|
|
||
|
void OvmsVehicleVWeUp::Ticker1(uint32_t ticker)
|
||
|
{
|
||
|
if (HasT26()) {
|
||
|
T26Ticker1(ticker);
|
||
|
}
|
||
|
|
||
|
// Entered charge phase topping off (v.b.soc > v.c.limit.soc)?
|
||
|
// Note: this is considered topping off also without timermode enabled,
|
||
|
// so we get a notification at our usual charge stop during range
|
||
|
// charging as well.
|
||
|
// Fallback for v.c.limit.soc is config xvu ctp.soclimit
|
||
|
if (StdMetrics.ms_v_charge_state->AsString() == "charging")
|
||
|
{
|
||
|
float soc = StdMetrics.ms_v_bat_soc->AsFloat();
|
||
|
int suff_soc = StdMetrics.ms_v_charge_limit_soc->AsInt();
|
||
|
if (m_chargestate_lastsoc <= suff_soc && soc > suff_soc) {
|
||
|
ESP_LOGI(TAG, "Ticker1: SOC crossed sufficient SOC limit (%d%%), entering topping off charge phase", suff_soc);
|
||
|
StdMetrics.ms_v_charge_state->SetValue("topoff");
|
||
|
}
|
||
|
m_chargestate_lastsoc = soc;
|
||
|
}
|
||
|
|
||
|
// Process a delayed timer mode change?
|
||
|
if (m_chargestate_ticker == 0 && m_chargestart_ticker == 0 && m_chargestop_ticker == 0 &&
|
||
|
m_timermode_ticker > 1 && --m_timermode_ticker == 1)
|
||
|
{
|
||
|
ESP_LOGI(TAG, "Ticker1: processing delayed charge timer mode update, new mode: %d", m_timermode_new);
|
||
|
bool modified = StdMetrics.ms_v_charge_timermode->SetValue(m_timermode_new);
|
||
|
UpdateChargeTimes();
|
||
|
|
||
|
// Send a notification if the timer mode is changed during a running charge:
|
||
|
if (modified && StdMetrics.ms_v_charge_inprogress->AsBool())
|
||
|
{
|
||
|
// Change charge state?
|
||
|
float soc = StdMetrics.ms_v_bat_soc->AsFloat();
|
||
|
int suff_soc = StdMetrics.ms_v_charge_limit_soc->AsInt();
|
||
|
bool modified_chargestate;
|
||
|
if (suff_soc > 0 && soc > suff_soc)
|
||
|
modified_chargestate = StdMetrics.ms_v_charge_state->SetValue("topoff");
|
||
|
else
|
||
|
modified_chargestate = StdMetrics.ms_v_charge_state->SetValue("charging");
|
||
|
m_chargestate_lastsoc = soc;
|
||
|
|
||
|
// If we changed the charge state, a state notification will be sent already.
|
||
|
// If not, send the dedicated timermode notification:
|
||
|
if (!modified_chargestate) {
|
||
|
StringWriter buf(200);
|
||
|
CommandStat(COMMAND_RESULT_NORMAL, &buf);
|
||
|
MyNotify.NotifyString("info", "charge.timermode", buf.c_str());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
m_timermode_ticker = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void OvmsVehicleVWeUp::Ticker10(uint32_t ticker)
|
||
|
{
|
||
|
// Send SOC monitoring log?
|
||
|
if (!IsOff())
|
||
|
{
|
||
|
static float last_soc = -1;
|
||
|
int storetime_days = MyConfig.GetParamValueInt("xvu", "log.socmon.storetime", 0);
|
||
|
if (storetime_days > 0 && (IsOn() || StdMetrics.ms_v_bat_soc->AsFloat() != last_soc))
|
||
|
{
|
||
|
MyNotify.NotifyStringf("data", "xvu.log.socmon",
|
||
|
"XVU-LOG-SOCMon,3,%d,%.1f,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.5f,%.5f,%.1f",
|
||
|
storetime_days * 86400,
|
||
|
StdMetrics.ms_v_bat_temp->AsFloat(),
|
||
|
IsCharging(),
|
||
|
IsOBDReady() ? BatMgmtSoCAbs->AsFloat() : 0,
|
||
|
IsOBDReady() ? MotElecSoCAbs->AsFloat() : 0,
|
||
|
IsOBDReady() ? ChgMgmtSoCNorm->AsFloat() : 0,
|
||
|
IsOBDReady() ? MotElecSoCNorm->AsFloat() : 0,
|
||
|
StdMetrics.ms_v_bat_soc->AsFloat(),
|
||
|
StdMetrics.ms_v_bat_voltage->AsFloat(),
|
||
|
StdMetrics.ms_v_bat_current->AsFloat(),
|
||
|
StdMetrics.ms_v_bat_soh->AsFloat(),
|
||
|
StdMetrics.ms_v_bat_cac->AsFloat(),
|
||
|
StdMetrics.ms_v_bat_energy_used_total->AsFloat(),
|
||
|
StdMetrics.ms_v_bat_energy_recd_total->AsFloat(),
|
||
|
StdMetrics.ms_v_bat_coulomb_used_total->AsFloat(),
|
||
|
StdMetrics.ms_v_bat_coulomb_recd_total->AsFloat(),
|
||
|
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_vstddev->AsFloat(),
|
||
|
StdMetrics.ms_v_bat_pack_vgrad->AsFloat(),
|
||
|
IsOBDReady() ? m_bat_energy_range->AsFloat() : 0);
|
||
|
last_soc = StdMetrics.ms_v_bat_soc->AsFloat();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void OvmsVehicleVWeUp::Ticker60(uint32_t ticker)
|
||
|
{
|
||
|
if (HasNoOBD()) {
|
||
|
UpdateChargeTimes();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* GetNotifyChargeStateDelay: framework hook
|
||
|
*/
|
||
|
int OvmsVehicleVWeUp::GetNotifyChargeStateDelay(const char *state)
|
||
|
{
|
||
|
if (strcmp(state, "charging") == 0) {
|
||
|
// On charge start, we need to delay the notification by 24 seconds to get the first
|
||
|
// stable battery current (→ charge durations & speed) reading after ramp up.
|
||
|
// Including the 6 second state change delay, this means the notification
|
||
|
// will be sent 30 seconds after the charge start.
|
||
|
// In case a user is interested in getting the notification as fast as possible,
|
||
|
// we provide a configuration option.
|
||
|
// If a timer mode change during a running charge implied switching topoff→charging,
|
||
|
// we allow an immediate notification (or with a running state ticker).
|
||
|
if (StdMetrics.ms_v_charge_time->AsInt() == 0)
|
||
|
return MyConfig.GetParamValueInt("xvu", "notify.charge.start.delay", 24);
|
||
|
else
|
||
|
return m_chargestate_ticker;
|
||
|
}
|
||
|
else if (strcmp(state, "topoff") == 0) {
|
||
|
// If the charge starts in the topping off section, use the standard start delay,
|
||
|
// if the charge is already in progress deliver the phase change notification
|
||
|
// immediately or using an already scheduled state change (to catch a topoff
|
||
|
// detection during a charge initialization phase):
|
||
|
if (StdMetrics.ms_v_charge_time->AsInt() == 0)
|
||
|
return MyConfig.GetParamValueInt("xvu", "notify.charge.start.delay", 24);
|
||
|
else
|
||
|
return m_chargestate_ticker;
|
||
|
}
|
||
|
else {
|
||
|
return 3;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* NotifiedVehicleChargeState: framework hook; charge state notifications have been sent
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::NotifiedVehicleChargeState(const char* state)
|
||
|
{
|
||
|
// Delayed clearing of the charge type (type2/ccs) after charging has stopped:
|
||
|
if (!StdMetrics.ms_v_charge_inprogress->AsBool()) {
|
||
|
SetChargeType(CHGTYPE_None);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* SetUsePhase: track phase transitions between charging & driving
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::SetUsePhase(use_phase_t use_phase)
|
||
|
{
|
||
|
if (m_use_phase == use_phase)
|
||
|
return;
|
||
|
|
||
|
// Phase transition: reset BMS statistics?
|
||
|
if (MyConfig.GetParamValueBool("xvu", "bms.autoreset")) {
|
||
|
ESP_LOGD(TAG, "SetUsePhase %d: resetting BMS statistics", use_phase);
|
||
|
BmsResetCellStats();
|
||
|
}
|
||
|
|
||
|
m_use_phase = use_phase;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* ResetTripCounters: called at trip start to set reference points
|
||
|
* Called by the connector subsystem detecting vehicle state changes,
|
||
|
* i.e. T26 has priority if available.
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::ResetTripCounters()
|
||
|
{
|
||
|
// Clear per trip counters:
|
||
|
StdMetrics.ms_v_pos_trip->SetValue(0);
|
||
|
m_odo_trip = 0;
|
||
|
m_tripfrac_reftime = esp_log_timestamp();
|
||
|
m_tripfrac_refspeed = StdMetrics.ms_v_pos_speed->AsFloat();
|
||
|
|
||
|
StdMetrics.ms_v_bat_energy_recd->SetValue(0);
|
||
|
StdMetrics.ms_v_bat_energy_used->SetValue(0);
|
||
|
StdMetrics.ms_v_bat_coulomb_recd->SetValue(0);
|
||
|
StdMetrics.ms_v_bat_coulomb_used->SetValue(0);
|
||
|
|
||
|
// Get trip start references as far as available:
|
||
|
// (if we don't have them yet, IncomingPollReply() will set them ASAP)
|
||
|
if (IsOBDReady()) {
|
||
|
m_soc_abs_start = BatMgmtSoCAbs->AsFloat();
|
||
|
}
|
||
|
m_soc_norm_start = StdMetrics.ms_v_bat_soc->AsFloat();
|
||
|
m_energy_recd_start = StdMetrics.ms_v_bat_energy_recd_total->AsFloat();
|
||
|
m_energy_used_start = StdMetrics.ms_v_bat_energy_used_total->AsFloat();
|
||
|
m_coulomb_recd_start = StdMetrics.ms_v_bat_coulomb_recd_total->AsFloat();
|
||
|
m_coulomb_used_start = StdMetrics.ms_v_bat_coulomb_used_total->AsFloat();
|
||
|
|
||
|
ESP_LOGD(TAG, "Trip start ref: socnrm=%f socabs=%f, er=%f, eu=%f, cr=%f, cu=%f",
|
||
|
m_soc_norm_start, m_soc_abs_start,
|
||
|
m_energy_recd_start, m_energy_used_start, m_coulomb_recd_start, m_coulomb_used_start);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* UpdateTripOdo: odometer resolution is only 1 km, so trip distances lack
|
||
|
* precision. To compensate, this method derives trip distance from speed.
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::UpdateTripOdo()
|
||
|
{
|
||
|
// Process speed update:
|
||
|
uint32_t now = esp_log_timestamp();
|
||
|
float speed = StdMetrics.ms_v_pos_speed->AsFloat();
|
||
|
|
||
|
if (m_tripfrac_reftime && now > m_tripfrac_reftime)
|
||
|
{
|
||
|
float speed_avg = ABS(speed + m_tripfrac_refspeed) / 2;
|
||
|
uint32_t time_ms = now - m_tripfrac_reftime;
|
||
|
double meters = speed_avg / 3.6 * time_ms / 1000;
|
||
|
m_odo_trip += meters / 1000;
|
||
|
StdMetrics.ms_v_pos_trip->SetValue(TRUNCPREC(m_odo_trip,3));
|
||
|
}
|
||
|
|
||
|
m_tripfrac_reftime = now;
|
||
|
m_tripfrac_refspeed = speed;
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* ResetChargeCounters: call at charge start to set reference points
|
||
|
* Called by the connector subsystem detecting vehicle state changes,
|
||
|
* i.e. T26 has priority if available.
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::ResetChargeCounters()
|
||
|
{
|
||
|
// Clear per charge counter:
|
||
|
StdMetrics.ms_v_charge_kwh->SetValue(0);
|
||
|
StdMetrics.ms_v_charge_kwh_grid->SetValue(0);
|
||
|
m_charge_kwh_grid = 0;
|
||
|
|
||
|
// Get charge start reference as far as available:
|
||
|
// (if we don't have it yet, IncomingPollReply() will set it ASAP)
|
||
|
if (IsOBDReady()) {
|
||
|
m_soc_abs_start = BatMgmtSoCAbs->AsFloat();
|
||
|
}
|
||
|
m_soc_norm_start = StdMetrics.ms_v_bat_soc->AsFloat();
|
||
|
m_energy_charged_start = StdMetrics.ms_v_bat_energy_recd_total->AsFloat();
|
||
|
m_coulomb_charged_start = StdMetrics.ms_v_bat_coulomb_recd_total->AsFloat();
|
||
|
m_charge_kwh_grid_start = StdMetrics.ms_v_charge_kwh_grid_total->AsFloat();
|
||
|
|
||
|
ESP_LOGD(TAG, "Charge start ref: socnrm=%f socabs=%f cr=%f er=%f gr=%f",
|
||
|
m_soc_norm_start, m_soc_abs_start, m_coulomb_charged_start,
|
||
|
m_energy_charged_start, m_charge_kwh_grid_start);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* SetChargeType: set current internal & public charge type (AC / DC / None)
|
||
|
* Controlled by the OBD handler if enabled (derived from VWUP_CHG_MGMT_HV_CHGMODE).
|
||
|
* The charge type defines the source for the charge metrics, to query the type
|
||
|
* use IsChargeModeAC() and IsChargeModeDC().
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::SetChargeType(chg_type_t chgtype)
|
||
|
{
|
||
|
if (m_chg_type == chgtype)
|
||
|
return;
|
||
|
|
||
|
m_chg_type = chgtype;
|
||
|
|
||
|
if (m_chg_type == CHGTYPE_AC) {
|
||
|
StdMetrics.ms_v_charge_type->SetValue("type2");
|
||
|
}
|
||
|
else if (m_chg_type == CHGTYPE_DC) {
|
||
|
StdMetrics.ms_v_charge_type->SetValue("ccs");
|
||
|
}
|
||
|
else {
|
||
|
StdMetrics.ms_v_charge_type->SetValue("");
|
||
|
// …and clear/reset charge metrics:
|
||
|
if (IsOBDReady()) {
|
||
|
ChargerPowerEffEcu->SetValue(100);
|
||
|
ChargerPowerLossEcu->SetValue(0);
|
||
|
ChargerPowerEffCalc->SetValue(100);
|
||
|
ChargerPowerLossCalc->SetValue(0);
|
||
|
ChargerAC1U->SetValue(0);
|
||
|
ChargerAC1I->SetValue(0);
|
||
|
ChargerAC2U->SetValue(0);
|
||
|
ChargerAC2I->SetValue(0);
|
||
|
ChargerACPower->SetValue(0);
|
||
|
ChargerDC1U->SetValue(0);
|
||
|
ChargerDC1I->SetValue(0);
|
||
|
ChargerDC2U->SetValue(0);
|
||
|
ChargerDC2I->SetValue(0);
|
||
|
ChargerDCPower->SetValue(0);
|
||
|
m_chg_ccs_voltage->SetValue(0);
|
||
|
m_chg_ccs_current->SetValue(0);
|
||
|
m_chg_ccs_power->SetValue(0);
|
||
|
}
|
||
|
StdMetrics.ms_v_charge_voltage->SetValue(0);
|
||
|
StdMetrics.ms_v_charge_current->SetValue(0);
|
||
|
StdMetrics.ms_v_charge_power->SetValue(0);
|
||
|
StdMetrics.ms_v_charge_efficiency->SetValue(0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* SetChargeState: set v.c.charging, v.c.state and v.c.substate according to current
|
||
|
* charge timer mode, limits and SOC
|
||
|
* Note: changing v.c.state triggers the notification, so this should be called last.
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::SetChargeState(bool charging)
|
||
|
{
|
||
|
bool timermode = StdMetrics.ms_v_charge_timermode->AsBool();
|
||
|
int soc = StdMetrics.ms_v_bat_soc->AsInt();
|
||
|
int socmin = IsOBDReady() ? m_chg_timer_socmin->AsInt() : 0;
|
||
|
int socmax = IsOBDReady() ? m_chg_timer_socmax->AsInt() : 0;
|
||
|
int suff_soc = StdMetrics.ms_v_charge_limit_soc->AsInt();
|
||
|
|
||
|
if (charging)
|
||
|
{
|
||
|
// Charge in progress:
|
||
|
StdMetrics.ms_v_charge_inprogress->SetValue(true);
|
||
|
|
||
|
if (timermode)
|
||
|
StdMetrics.ms_v_charge_substate->SetValue("scheduledstart");
|
||
|
else
|
||
|
StdMetrics.ms_v_charge_substate->SetValue("onrequest");
|
||
|
|
||
|
// Topping off?
|
||
|
if (suff_soc > 0 && soc > suff_soc)
|
||
|
StdMetrics.ms_v_charge_state->SetValue("topoff");
|
||
|
else
|
||
|
StdMetrics.ms_v_charge_state->SetValue("charging");
|
||
|
|
||
|
m_chargestate_lastsoc = soc;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Charge stopped:
|
||
|
StdMetrics.ms_v_charge_inprogress->SetValue(false);
|
||
|
|
||
|
if (timermode)
|
||
|
{
|
||
|
// Scheduled charge;
|
||
|
// if stopped at maximum SOC, we've finished as scheduled:
|
||
|
if (soc >= socmax-1 && soc <= socmax+1) {
|
||
|
StdMetrics.ms_v_charge_substate->SetValue("scheduledstop");
|
||
|
StdMetrics.ms_v_charge_state->SetValue("done");
|
||
|
}
|
||
|
// …if stopped at minimum SOC, we're waiting for the second phase:
|
||
|
else if (soc >= socmin-1 && soc <= socmin+1) {
|
||
|
StdMetrics.ms_v_charge_substate->SetValue("timerwait");
|
||
|
StdMetrics.ms_v_charge_state->SetValue("stopped");
|
||
|
}
|
||
|
// …else the charge has been interrupted:
|
||
|
else {
|
||
|
StdMetrics.ms_v_charge_substate->SetValue("interrupted");
|
||
|
StdMetrics.ms_v_charge_state->SetValue("stopped");
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Unscheduled charge; done if fully charged:
|
||
|
if (soc >= 99) {
|
||
|
StdMetrics.ms_v_charge_substate->SetValue("stopped");
|
||
|
StdMetrics.ms_v_charge_state->SetValue("done");
|
||
|
}
|
||
|
// …else the charge has been interrupted:
|
||
|
else {
|
||
|
StdMetrics.ms_v_charge_substate->SetValue("interrupted");
|
||
|
StdMetrics.ms_v_charge_state->SetValue("stopped");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* CalcChargeTime: charge time prediction
|
||
|
*/
|
||
|
int OvmsVehicleVWeUp::CalcChargeTime(float capacity, float max_pwr, int from_soc, int to_soc)
|
||
|
{
|
||
|
struct {
|
||
|
int soc; float pwr; float grd;
|
||
|
} ccurve[] = {
|
||
|
{ 0, 30.0, (32.5-30.0) / ( 30- 0) },
|
||
|
{ 30, 32.5, (26.5-32.5) / ( 55- 30) },
|
||
|
{ 55, 26.5, (18.5-26.5) / ( 76- 55) },
|
||
|
{ 76, 18.5, (11.0-18.5) / ( 81- 76) },
|
||
|
{ 81, 11.0, ( 6.5-11.0) / ( 91- 81) },
|
||
|
{ 91, 6.5, ( 3.0- 6.5) / (100- 91) },
|
||
|
{ 100, 3.0, 0 },
|
||
|
};
|
||
|
const int csize = sizeof_array(ccurve);
|
||
|
|
||
|
if (capacity <= 0 || to_soc <= from_soc)
|
||
|
return 0;
|
||
|
|
||
|
// Find curve section for a given SOC:
|
||
|
auto find_csection = [&](int soc) {
|
||
|
if (soc == 0) return 0;
|
||
|
int i;
|
||
|
for (i = 0; i < csize; i++) {
|
||
|
if (ccurve[i].soc < soc && ccurve[i+1].soc >= soc)
|
||
|
break;
|
||
|
}
|
||
|
return i;
|
||
|
};
|
||
|
|
||
|
// Calculate the theoretical max power at a curve section SOC point:
|
||
|
auto calc_cpwr = [&](int ci, int soc) {
|
||
|
return ccurve[ci].pwr + (soc - ccurve[ci].soc) * ccurve[ci].grd;
|
||
|
};
|
||
|
|
||
|
// Calculate the theoretical max power at a curve section SOC point:
|
||
|
auto pwr_limit = [&](float pwr) {
|
||
|
return (max_pwr > 0) ? std::min(pwr, max_pwr) : pwr;
|
||
|
};
|
||
|
|
||
|
// Sum charge curve section parts involved:
|
||
|
|
||
|
int from_section = find_csection(from_soc),
|
||
|
to_section = find_csection(to_soc);
|
||
|
float charge_time = 0;
|
||
|
|
||
|
int s1, s2;
|
||
|
float p1, p2;
|
||
|
float section_energy, section_time;
|
||
|
|
||
|
for (int section = from_section; section <= to_section; section++)
|
||
|
{
|
||
|
if (section == from_section) {
|
||
|
s1 = from_soc;
|
||
|
p1 = calc_cpwr(from_section, from_soc);
|
||
|
} else {
|
||
|
s1 = ccurve[section].soc;
|
||
|
p1 = ccurve[section].pwr;
|
||
|
}
|
||
|
|
||
|
if (section == to_section) {
|
||
|
s2 = to_soc;
|
||
|
p2 = calc_cpwr(to_section, to_soc);
|
||
|
} else {
|
||
|
s2 = ccurve[section+1].soc;
|
||
|
p2 = ccurve[section+1].pwr;
|
||
|
}
|
||
|
|
||
|
p1 = pwr_limit(p1);
|
||
|
p2 = pwr_limit(p2);
|
||
|
|
||
|
section_energy = capacity * (s2 - s1) / 100.0;
|
||
|
section_time = section_energy / ((p1 + p2) / 2);
|
||
|
charge_time += section_time;
|
||
|
}
|
||
|
|
||
|
// return full minutes:
|
||
|
return std::ceil(charge_time * 60);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* UpdateChargeTimes: update all charge time predictions
|
||
|
* This is called by Ticker60() and by IncomingPollReply(), and on config changes.
|
||
|
* While charging, the car delivers a CTP for the current SOC limit if set, or 100%,
|
||
|
* but only if the OBD connection is available.
|
||
|
*/
|
||
|
void OvmsVehicleVWeUp::UpdateChargeTimes()
|
||
|
{
|
||
|
float capacity = 0, max_pwr = 0;
|
||
|
int from_soc = 0, to_soc = 0;
|
||
|
|
||
|
if (IsOBDReady())
|
||
|
capacity = m_bat_cap_kwh_norm->AsFloat();
|
||
|
if (capacity <= 0)
|
||
|
capacity = (vweup_modelyear > 2019) ? 32.3 : 16.4;
|
||
|
|
||
|
if (IsCharging())
|
||
|
max_pwr = -StdMetrics.ms_v_bat_power->AsFloat();
|
||
|
if (max_pwr <= 0)
|
||
|
max_pwr = MyConfig.GetParamValueFloat("xvu", "ctp.maxpower", 0);
|
||
|
|
||
|
// If timermode is configured for a maximum SOC < 100%, that will also be the
|
||
|
// sufficient SOC for the charge. With timer mode disabled or destination SOC
|
||
|
// at 100%, the sufficient SOC limit will be used from the user preferences.
|
||
|
|
||
|
bool timermode = StdMetrics.ms_v_charge_timermode->AsBool();
|
||
|
int timer_socmax = IsOBDReady() ? m_chg_timer_socmax->AsInt() : 0;
|
||
|
|
||
|
// Set v.c.limit.soc (sufficient SOC for current charge):
|
||
|
int suff_soc = timer_socmax;
|
||
|
if (timer_socmax == 0 || timer_socmax == 100)
|
||
|
suff_soc = MyConfig.GetParamValueFloat("xvu", "ctp.soclimit", 80);
|
||
|
StdMetrics.ms_v_charge_limit_soc->SetValue(suff_soc);
|
||
|
|
||
|
// Calculate charge times for 100% and…
|
||
|
from_soc = StdMetrics.ms_v_bat_soc->AsInt();
|
||
|
to_soc = suff_soc;
|
||
|
|
||
|
if (IsCharging() && m_chg_ctp_car >= 0 && m_chg_ctp_car < 630) {
|
||
|
// The car's CTP always relates to the effective charge destination SOC:
|
||
|
if (timermode && timer_socmax < 100) {
|
||
|
*StdMetrics.ms_v_charge_duration_soc = m_chg_ctp_car;
|
||
|
*StdMetrics.ms_v_charge_duration_full = CalcChargeTime(capacity, max_pwr, from_soc, 100);
|
||
|
} else {
|
||
|
*StdMetrics.ms_v_charge_duration_soc = CalcChargeTime(capacity, max_pwr, from_soc, to_soc);
|
||
|
*StdMetrics.ms_v_charge_duration_full = m_chg_ctp_car;
|
||
|
}
|
||
|
} else {
|
||
|
// Car CTP not available, do our calculation for both:
|
||
|
*StdMetrics.ms_v_charge_duration_soc = CalcChargeTime(capacity, max_pwr, from_soc, to_soc);
|
||
|
*StdMetrics.ms_v_charge_duration_full = CalcChargeTime(capacity, max_pwr, from_soc, 100);
|
||
|
}
|
||
|
|
||
|
// Derive charge mode from final SOC destination:
|
||
|
if (!timermode || timer_socmax == 100)
|
||
|
StdMetrics.ms_v_charge_mode->SetValue("range");
|
||
|
else
|
||
|
StdMetrics.ms_v_charge_mode->SetValue("standard");
|
||
|
}
|