From 047318cd305f7a05f2fcfd8b58530e6a54abf520 Mon Sep 17 00:00:00 2001 From: Shankari Date: Wed, 20 Nov 2024 02:27:15 -0800 Subject: [PATCH] Make the EV-computed curve more meaningful - Do not throttle the SASchedule from the EV based on departure time, only transmit the composite pmax schedule. The EV will deareate based on departure time - Show the values in the current power delivery request instead of the progress towards the eamount so that we can see the impact of curtailing the pmax - Pass in the pmax to the power curve computation algo, although it is currently a NOP This fixes: https://github.com/EVerest/everest-demo/issues/92#issuecomment-2487836225 Signed-off-by: Shankari --- manager/demo-patches/enable_iso_dt.patch | 98 +++++-------------- manager/demo-patches/power_curve.py | 14 +-- manager/demo-patches/simulator_enabledt.patch | 33 ++++--- 3 files changed, 50 insertions(+), 95 deletions(-) diff --git a/manager/demo-patches/enable_iso_dt.patch b/manager/demo-patches/enable_iso_dt.patch index ee39155b..ccc5f917 100644 --- a/manager/demo-patches/enable_iso_dt.patch +++ b/manager/demo-patches/enable_iso_dt.patch @@ -1,27 +1,11 @@ --- /ext/source/modules/EvseV2G/iso_server.cpp +++ /ext/source/modules/EvseV2G/iso_server.cpp -@@ -37,6 +37,7 @@ - #define MQTT_MAX_PAYLOAD_SIZE 268435455 - #define V2G_SECC_MSG_CERTINSTALL_TIME 4500 - #define GEN_CHALLENGE_SIZE 16 -+#define CEIL(x, y) (x)/(y) + ((x) % (y) != 0); - - constexpr uint16_t SAE_V2H = 28472; - constexpr uint16_t SAE_V2G = 28473; -@@ -1072,6 +1073,7 @@ - } - res->EVSEProcessing = (iso2_EVSEProcessingType)conn->ctx->evse_v2g_data.evse_processing[PHASE_AUTH]; - -+ - if (conn->ctx->evse_v2g_data.evse_processing[PHASE_AUTH] != iso2_EVSEProcessingType_Finished) { - if (((is_payment_option_contract == false) && (conn->ctx->session.auth_timeout_eim == 0)) || - ((is_payment_option_contract == true) && (conn->ctx->session.auth_timeout_pnc == 0))) { -@@ -1130,32 +1132,84 @@ +@@ -1130,32 +1130,55 @@ static enum v2g_event handle_iso_charge_parameter_discovery(struct v2g_connectio res->EVSEChargeParameter_isUsed = 0; res->EVSEProcessing = (iso2_EVSEProcessingType)conn->ctx->evse_v2g_data.evse_processing[PHASE_PARAMETER]; + struct linked_ac_params { -+ float max_current; ++ float max_current; + int64_t voltage; + int64_t pmax; + }; @@ -39,75 +23,48 @@ /* Determin max current and nominal voltage */ - float max_current = conn->ctx->basic_config.evse_ac_current_limit; - int64_t nom_voltage = -+ linked_ac_params default_params; ++ linked_ac_params default_params; + /* Setup default params (before the departure time overrides) */ + default_params.max_current = conn->ctx->basic_config.evse_ac_current_limit; + default_params.voltage = conn->ctx->evse_v2g_data.evse_nominal_voltage.Value * pow(10, conn->ctx->evse_v2g_data.evse_nominal_voltage.Multiplier); /* nominal voltage */ - - /* Calculate pmax based on max current, nominal voltage and phase count (which the car has selected - * above) */ +- +- /* Calculate pmax based on max current, nominal voltage and phase count (which the car has selected +- * above) */ - int64_t pmax = - max_current * nom_voltage * -+ default_params.pmax = ++ default_params.pmax = + default_params.max_current * default_params.voltage * ((req->RequestedEnergyTransferMode == iso2_EnergyTransferModeType_AC_single_phase_core) ? 1 : 3); -- populate_physical_value(&conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0] -+ dlog(DLOG_LEVEL_WARNING, "before adjusting for departure time, max_current %f, nom_voltage %d, pmax %d, departure_duration %d", ++ ++ dlog(DLOG_LEVEL_WARNING, "before adjusting for departure time, max_current %f, nom_voltage %d, pmax %d, departure_duration %d", + default_params.max_current, default_params.voltage, + default_params.pmax, departure_time_duration); -+ double req_eamount = calc_physical_value(req->AC_EVChargeParameter.EAmount.Value, ++ double req_eamount = calc_physical_value(req->AC_EVChargeParameter.EAmount.Value, + req->AC_EVChargeParameter.EAmount.Multiplier); -+ dlog(DLOG_LEVEL_WARNING, "Requested departure time %u, requested energy %f", ++ dlog(DLOG_LEVEL_WARNING, "Requested departure time %u, requested energy %f", + departure_time_duration, req_eamount); -+ if (departure_time_duration == 0) { -+ dlog(DLOG_LEVEL_WARNING, "No departure time specified, not modifying defaults"); -+ sel_params = default_params; -+ } else { -+ dlog(DLOG_LEVEL_WARNING, "Departure time specified, checking to see if we can lower requirements"); -+ int64_t departure_time_duration_hours = departure_time_duration / 3600; -+ float min_hours_to_charge = req_eamount / default_params.pmax; -+ float min_secs_to_charge = min_hours_to_charge * 60 * 60; -+ if (min_secs_to_charge >= departure_time_duration) { -+ dlog(DLOG_LEVEL_WARNING, -+ "Min hours to charge %f, requested departure time in hours %f, pmax is unchanged", -+ min_hours_to_charge, departure_time_duration_hours); -+ sel_params = default_params; -+ } else { -+ /* In general, while computing these values, we use integer division -+ which rounds down. We then check if there is a reminder and add one to round up. -+ The rationale is that, from a user perspective, it is better to finish charging -+ a teeny bit early rather than not provide all the energy requested */ -+ sel_params.pmax = (int)std::ceil(req_eamount / departure_time_duration_hours); -+ sel_params.voltage = default_params.voltage; -+ sel_params.max_current = CEIL(sel_params.pmax, (sel_params.voltage * -+ ((req->RequestedEnergyTransferMode == iso2_EnergyTransferModeType_AC_single_phase_core) ? 1 : 3))); -+ dlog(DLOG_LEVEL_WARNING, -+ "Min hours to charge %f, requested departure time in hours %d, plenty of time to charge", -+ min_hours_to_charge, departure_time_duration_hours); -+ dlog(DLOG_LEVEL_WARNING, "lowering pmax = %d, max_current = %f, pmax float = %f", -+ sel_params.pmax, sel_params.max_current, ((float)req_eamount / (float)departure_time_duration_hours)); -+ } -+ } -+ populate_physical_value(&conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0] ++ sel_params = default_params; ++ + populate_physical_value(&conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0] .PMaxSchedule.PMaxScheduleEntry.array[0] .PMax, - pmax, iso2_unitSymbolType_W); -+ sel_params.pmax, iso2_unitSymbolType_W); ++ sel_params.pmax, iso2_unitSymbolType_W); } else { conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0] .PMaxSchedule.PMaxScheduleEntry.array[0] .PMax = conn->ctx->evse_v2g_data.evse_maximum_power_limit; } -+ if (departure_time_duration == 0) { ++ if (departure_time_duration == 0) { + departure_time_duration = SA_SCHEDULE_DURATION; // one day, per spec -+ } ++ } + conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0] .PMaxSchedule.PMaxScheduleEntry.array[0] .RelativeTimeInterval.start = 0; -@@ -1164,7 +1218,7 @@ +@@ -1164,7 +1187,7 @@ static enum v2g_event handle_iso_charge_parameter_discovery(struct v2g_connectio .RelativeTimeInterval.duration_isUsed = 1; conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0] .PMaxSchedule.PMaxScheduleEntry.array[0] @@ -116,22 +73,15 @@ conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0] .PMaxSchedule.PMaxScheduleEntry.arrayLen = 1; conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.arrayLen = 1; -@@ -1207,19 +1261,15 @@ - - populate_ac_evse_status(conn->ctx, &res->AC_EVSEChargeParameter.AC_EVSEStatus); - -+ // We have already calculated all of these above, so let's not duplicate code here - /* Max current */ -- float max_current = conn->ctx->basic_config.evse_ac_current_limit; -- populate_physical_value_float(&res->AC_EVSEChargeParameter.EVSEMaxCurrent, max_current, 1, -+ populate_physical_value_float(&res->AC_EVSEChargeParameter.EVSEMaxCurrent, sel_params.max_current, 1, +@@ -1212,14 +1235,8 @@ static enum v2g_event handle_iso_charge_parameter_discovery(struct v2g_connectio + populate_physical_value_float(&res->AC_EVSEChargeParameter.EVSEMaxCurrent, max_current, 1, iso2_unitSymbolType_A); -- - /* Nominal voltage */ - res->AC_EVSEChargeParameter.EVSENominalVoltage = conn->ctx->evse_v2g_data.evse_nominal_voltage; + +- /* Nominal voltage */ +- res->AC_EVSEChargeParameter.EVSENominalVoltage = conn->ctx->evse_v2g_data.evse_nominal_voltage; - int64_t nom_voltage = conn->ctx->evse_v2g_data.evse_nominal_voltage.Value * - pow(10, conn->ctx->evse_v2g_data.evse_nominal_voltage.Multiplier); - +- /* Calculate pmax based on max current, nominal voltage and phase count (which the car has selected above) */ - int64_t pmax = max_current * nom_voltage * - ((iso2_EnergyTransferModeType_AC_single_phase_core == req->RequestedEnergyTransferMode) ? 1 : 3); diff --git a/manager/demo-patches/power_curve.py b/manager/demo-patches/power_curve.py index bf0a58b3..186426e3 100644 --- a/manager/demo-patches/power_curve.py +++ b/manager/demo-patches/power_curve.py @@ -15,7 +15,7 @@ @author: ANAND """ # KS is btwn 1 and 20 -def LQRChargeCurve(DepTime, EAmount, KS): +def LQRChargeCurve(DepTime, EAmount, PMax, KS): # system matrices A=np.array([[0]]) B=np.array([[1]]) @@ -53,6 +53,7 @@ def LQRChargeCurve(DepTime, EAmount, KS): # define the input for closed-loop simulation inputCL=np.zeros(shape=(1,numberSamples)) inputCL[0,:]=xd*np.ones(numberSamples) + print(f"Created input array with {EAmount=} and {numberSamples=}") returnSimulationCL = ct.forced_response(sysStateSpaceCl, timeVector, inputCL, @@ -76,14 +77,13 @@ def LQRChargeCurve(DepTime, EAmount, KS): @author Katie ''' -def formatCurveData(yc, uc, tc): +def formatCurveData(profile_entry_list): # Node-RED expects watts & miliseconds - yc_curve = [{"x": float(x), "y": float(y) * 1000} for x, y in zip(yc, tc)] - uc_curve = [{"x": float(x), "y": float(y) * 1000} for x, y in zip(uc, tc)] + yc_curve = [{"x": float(ped.start), "y": float(ped.max_power.value)} for ped in profile_entry_list] return { - "series": ["A", "B"], - "data": [yc_curve, uc_curve], - "labels": ["r1", "r2"] + "series": ["A"], + "data": [yc_curve], + "labels": ["r1"] } diff --git a/manager/demo-patches/simulator_enabledt.patch b/manager/demo-patches/simulator_enabledt.patch index e09bd3e6..865ae306 100644 --- a/manager/demo-patches/simulator_enabledt.patch +++ b/manager/demo-patches/simulator_enabledt.patch @@ -1,5 +1,5 @@ ---- /ext/dist/libexec/everest/3rd_party/josev/iso15118/evcc/controller/simulator.py -+++ /ext/dist/libexec/everest/3rd_party/josev/iso15118/evcc/controller/simulator.py +--- /ext/dist/libexec/everest/3rd_party/josev/iso15118/evcc/controller/simulator.py 2024-11-20 09:51:36.878132628 +0000 ++++ /ext/dist/libexec/everest/3rd_party/josev/iso15118/evcc/controller/simulator.py 2024-11-20 09:51:36.878132628 +0000 @@ -7,6 +7,7 @@ import logging import random @@ -32,7 +32,7 @@ from iso15118.shared.settings import get_PKI_PATH logger = logging.getLogger(__name__) -@@ -283,7 +294,7 @@ +@@ -290,7 +301,7 @@ dc_charge_params = None if (await self.get_energy_transfer_mode(protocol)).startswith("AC"): @@ -41,7 +41,7 @@ unit=UnitSymbol.WATT_HOURS) ev_max_voltage = PVEVMaxVoltage( multiplier=0, value=400, unit=UnitSymbol.VOLTAGE -@@ -295,7 +306,7 @@ +@@ -302,7 +313,7 @@ multiplier=0, value=10, unit=UnitSymbol.AMPERE ) ac_charge_params = ACEVChargeParameter( @@ -50,7 +50,7 @@ e_amount=e_amount, ev_max_voltage=ev_max_voltage, ev_max_current=ev_max_current, -@@ -306,7 +317,7 @@ +@@ -313,7 +324,7 @@ multiplier=1, value=6000, unit=UnitSymbol.WATT_HOURS ) dc_charge_params = DCEVChargeParameter( @@ -59,7 +59,7 @@ dc_ev_status=await self.get_dc_ev_status(), ev_maximum_current_limit=self.dc_ev_charge_params.dc_max_current_limit, ev_maximum_power_limit=self.dc_ev_charge_params.dc_max_power_limit, -@@ -422,7 +433,7 @@ +@@ -429,7 +440,7 @@ ) scheduled_params = ScheduledScheduleExchangeReqParams( @@ -68,7 +68,7 @@ ev_target_energy_request=RationalNumber(exponent=3, value=10), ev_max_energy_request=RationalNumber(exponent=3, value=20), ev_min_energy_request=RationalNumber(exponent=-2, value=5), -@@ -436,7 +447,7 @@ +@@ -443,7 +454,7 @@ ) -> DynamicScheduleExchangeReqParams: """Overrides EVControllerInterface.get_dynamic_se_params().""" dynamic_params = DynamicScheduleExchangeReqParams( @@ -77,7 +77,7 @@ min_soc=30, target_soc=80, ev_target_energy_request=RationalNumber(exponent=3, value=40), -@@ -577,12 +588,15 @@ +@@ -584,12 +595,15 @@ return schedule.sa_schedule_tuple_id async def process_sa_schedules_v2( @@ -94,18 +94,22 @@ # The charging schedule coming from the SECC is called 'schedule', the # pendant coming from the EVCC (after having processed the offered # schedule(s)) is called 'profile'. Therefore, we use the prefix -@@ -610,13 +624,43 @@ +@@ -617,13 +631,48 @@ ) evcc_profile_entry_list.append(last_profile_entry_details) + print("Done processing schedules...") + # Set Curve Variables... ++ print("About to handle pmax schedule %s" % secc_schedule.p_max_schedule.schedule_entries[0]) ++ p_max = secc_schedule.p_max_schedule.schedule_entries[0].p_max ++ pmax:float = p_max.value * pow(10, p_max.multiplier) ++ print(f"{pmax=}") + departure_time = secc_schedule.p_max_schedule.schedule_entries[0].time_interval.duration + new_schedule = evcc_profile_entry_list + print("Abt to generate Curve...", time_elapsed) + if (time_elapsed > departure_time): + print("End of Profile! Defaulting to EVCC profile enteries") -+ elif (departure_time != 86400): ++ else: + ks = 1 + # Check EAmount + mqtt_publish.single("everest_external/nodered/{}/evcc/check_eamount", "test", hostname="mqtt-server") @@ -119,13 +123,14 @@ + ks = 10 + else: # == algorithm_two + ks = 1 -+ power_draw_progress, power_draw, time_vector = LQRChargeCurve(departure_time, eamount, ks) -+ formatted_curve = formatCurveData(power_draw_progress,power_draw,time_vector) ++ power_draw_progress, power_draw, time_vector = LQRChargeCurve(departure_time, eamount, pmax, ks) + print(f"About to generate a new schedule with a EVCC_Profile {evcc_profile_entry_list}") + new_schedule = generate_new_schedule(evcc_profile_entry_list, power_draw, time_vector, departure_time, time_elapsed) + print(f"New schedule of length {len(new_schedule)} created") + ++ formatted_curve = formatCurveData(new_schedule) + # Then Re-Publish the chosen curve as the final selection ++ print(f"About to publish {str(formatted_curve)=}") + mqtt_publish.single("everest_external/nodered/{}/evcc/active_powercurve", str(formatted_curve), hostname="mqtt-server") + # TODO If a SalesTariff is present and digitally signed (and TLS is used), @@ -139,7 +144,7 @@ ) async def continue_charging(self) -> bool: -@@ -753,7 +797,7 @@ +@@ -760,7 +809,7 @@ else: # Dynamic Mode dynamic_params = DynamicACChargeLoopReqParams( @@ -148,7 +153,7 @@ ev_target_energy_request=RationalNumber(exponent=3, value=40), ev_max_energy_request=RationalNumber(exponent=3, value=60), ev_min_energy_request=RationalNumber(exponent=3, value=-20), -@@ -905,7 +949,7 @@ +@@ -912,7 +961,7 @@ multiplier=1, value=6000, unit=UnitSymbol.WATT_HOURS ) dc_charge_params = DCEVChargeParameter(