/* ; Project: Open Vehicle Monitor System ; Subproject: Integration of support for the VW e-UP ; ; (c) 2021 sharkcow , Chris van der Meijden, SokoFromNZ, Michael Balzer ; ; 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 static const char *TAG = "v-vweup"; #define VERSION "0.22.4" #include #include #include #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("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"); }