Closed shankari closed 3 weeks ago
Note that these are still very simple scenarios that are primarily focused on the communication between the station and the car. Concretely, the following scenarios are out of scope:
NotifyEVChargingNeedsRequest
to the CSMS; from our previous investigation, neither MaEVe or Citrine support this properly (https://github.com/EVerest/everest-demo/issues/44#issuecomment-2127702608). And Citrine doesn't work with PnC right now anyway.There are lots of cool things we can do in terms of renegotiation and I fully agree that is where the power in smart charging is. But we are going to take one step at a time and bring in more and more complex functionality into the demo over time, involving the community, and making sure that it is consistent with what stakeholders expect. Note the final scenario here, where the EVSE is not able to meet the requested schedule - is that what we want the EVSE to return?
The goal of these initial demos is to build interest and engagement and help spearhead those conversations.
From a technical perspective, this demo boils down to the following technical tasks:
We will update this issue with progress as we work on these tasks
Looking through the patches that I slapped together quickly, the callback in the core module that handles the set limits or charge profile calls from the CSMS is in modules/OCPP201/OCPP201.cpp
which essentially calls
this->set_external_limits(charging_schedules);
this->publish_charging_schedules(charging_schedules);
set_external_limits
calls this->r_evse_manager.at(evse_id - 1)->call_set_external_limits(limits);
publish_charging_schedules
calls publish_charging_schedules
on the ocppImplBase
base implementation. However, I don't know where that implementation actually is. I do find a default base implementation in modules/OCPP201/ocpp_generic/ocppImpl.hpp/cpp
, but there is no implementation of publish_charging_schedules
in itCouple of other notes:
modules/OCPP201/OCPP201.cpp
seems to be a direct copy of modules/OCPP/OCPP.cpp
!!ocppImplBase
. Searching for it gives the pointers and the subclasses, but not the original class. Given the name, I guess that it is the hookup between the module and the library.Going to focus on the EVSE manager's call_set_external_limits
for now since:
That looks like it calls through to the local_energy_limits
variable in the EvseManager - e.g.
https://github.com/EVerest/everest-core/blob/4b13b10c4b6ba7ae3edf0a9f7ddfdce274a9332a/modules/EvseManager/EvseManager.cpp#L1084
so here's where the ISO messages are sent out https://github.com/EVerest/everest-core/blob/4b13b10c4b6ba7ae3edf0a9f7ddfdce274a9332a/modules/EvseV2G/iso_server.cpp#L1105
it's a little bit complex, and it looks like we only populate one PMaxSchedule!!
conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
.PMaxSchedule.PMaxScheduleEntry.array[0]
.RelativeTimeInterval.start = 0;
conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
.PMaxSchedule.PMaxScheduleEntry.array[0]
.RelativeTimeInterval.duration_isUsed = 1;
conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
.PMaxSchedule.PMaxScheduleEntry.array[0]
.RelativeTimeInterval.duration = SA_SCHEDULE_DURATION;
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;
Need to add more logs to figure out what is going in here for our current working scenario, and then how we can extend it going forward....
Tracing through the repos to see where we'll need to add DepartureTime
. It helps to know that this is of the type "EVChargeParameterType", and is an optional field of of the AC_EVChargeParameterType
. Knowing this, we can go through the repos to figure out what needs changed.
It seems there are three variations of the AC_EVChargeParameterType
that are defined:
Tracing through now to see exactly when these EVChargeParameterTypes are used, should shed some light on exactly where changes are needed
Drawing out a call graph, mostly to make sure I'm not missing anything when tracing through the code. I'll keep the diagrams updated below a fold, if folks are curious about my process.
EDIT: Since we can ignore ext-openv2g
, I've updated the graph accordingly. See edits below the cut.
ok so the evse_sa_schedule_list
is set in modules/EvseV2G/v2g_ctx.cpp
, but I only see one entry being set, and I don't see how it is hooked up to the local_energy_limits
.
What I see is that there is a MQTT message to set the limits.
set_limit_amps
callback calls evse->call_set_external_limits
# grep -rl set_external_limits modules | grep -v OCPP
modules/API/API.cpp
modules/EvseManager/tests/EvseManagerStub.hpp
modules/EvseManager/evse/evse_managerImpl.cpp
modules/EvseManager/evse/evse_managerImpl.hpp
modules/EnergyNode/energy_grid/energyImpl.hpp
modules/EnergyNode/energy_grid/energyImpl.cpp
modules/EnergyNode/external_limits/external_energy_limitsImpl.cpp
modules/EnergyNode/external_limits/external_energy_limitsImpl.hpp
modules/EnergyNode/external_limits/external_energy_limitsImpl.cpp
calls mod->signalExternalLimit(value);
in modules/EnergyNode/energy_grid/energyImpl.cpp
which calls void energyImpl::set_external_limits
In the EVSE manager, I see
void evse_managerImpl::handle_set_external_limits(types::energy::ExternalLimits& value) {
mod->updateLocalEnergyLimit(value);
}
I do note
// wait for EnergyManager to assign optimized current on next opimizer run
commented in some logs from the energy manager, let's see how the clamping actually gets passed through...
Fails with
2024-07-16 15:11:25.227606 [WARN] ocpp:OCPP201 ocpp::DateTime ocpp::v201::SmartChargingHandler::get_next_temp_time(ocpp::DateTime, const std::vector<ocpp::v201::ChargingProfile>&, int32_t) :: Charging Profiles with more than one ChargingSchedule are not currently supported.
But now the limits are set to 0A. Why? It looks like it finds the correct period. I bet it is because the current is below the min (should add logs to verify). But for now, we can just bump up the amps....
Again, we get a single SA schedule with 6900W for one day (86400 duration). But we are only requesting 60 Wh. Let's bump that up to 60 kWh and see what happens.
Keeping a tally of the locations I'll need to add DepartureTime. First, just consolidating the iso requirements for my ease of reading...
With the info above, it seems we need to send & handle the DepartureTime whenever a ChargeParameterDiscoverReq is being sent. As I understand it, this will occur (i) when a new session has been started, and (ii) when a session has been resumed after a pause.
Now to hunt down these occurrences in PyEVJosev and JsEvManager. At a glance, the JsEvManager changes seem like they will be somewhat straightforward...
Changing the defaults in build/dist/libexec/everest/3rd_party/josev/iso15118/secc/states/iso15118_2_states.py
to bump up the power and set the departure time to see if it makes a difference...
p_e_amount: float = e_amount.value * pow(10, e_amount.multiplier + 3)
departure_time = (
ev_data_context.departure_time if ev_data_context.departure_time else 21600
)
Even after reverting changes.
Restarting even further after saving a patch with the logs
Starting with JsEvManager/index.js
some observations:
mod.uses.ev_board_support.call.set_cp_state({ cp_state: x}), where x is either
Cor
B`. I'm not sure this is the right lead, but I believe following calls like this may help figure out where we need to incorperate departureTime.From what I can gather, the mutation of variables like mod.maxCurrent
is handled by external packages -- as such, I don't believe we need to make any other changes to this file beyond setting the default of mod.iso_departure_time = null
...
Restarted everything. It works again.
I note that the construction of the charge parameter discovery request/response also has some SA schedule creation
sa_schedule_list = await self.comm_session.evse_controller.get_sa_schedule_list(
ev_data_context,
self.comm_session.config.free_charging_service,
max_schedule_entries,
departure_time,
)
But the current message doesn't seem to use that so we will ignore for simplicity
Taking some time to set up the Node-RED UI, I think that's the best place to start (knowing what signals we can / will send will help define the message behavior, and so on as the call graph grows). Some initial notes on the UI nodes available:
Here is where we run into our first design quirk. Per [V2H2-215]...
If there is no DepartureTime information available, the EVCC shall not include this message in the ChargeParameterDiscoveryReq message
This means we cannot simply have 0
be our base case for the input component (or use some value like -1 as the "null" - this value must be unsigned).
ChargeParameterDiscoveryReq
. Without piping through any of the messages, below is a mock up of the module
When sketching up these values, I'm left one concern. As sketched up, the proposed "setDepartureTime" command will send every time the number is set. How can we make sure that we only change the DepartureTime
value when starting or resuming a charging session? Will have to spend more time with JsEVManager to figure out how this will look in practice.
Still getting used to the Node-RED workflow, setting up the message flow -- writing these notes down mostly for my own understanding...
As I understand it, the Buffer sim commands function node takes the inputs from each of the input nodes, and adjusts the payload according to the topic.
After some searching, it seems the sim_commands
topic is defined within the injection of the CarSimulation node, but I cannot find the flows for sim_commands_pause
, etc... Am I misunderstanding how the flow get command works? Where are these being defined?
This leads me to the question: do I even need to filter through the buffer?? For example, the values set by the MaxCurrentSlider aren't being filtered. I'm going to just assume we don't have to filter it for now, I think that should be OK...
Aha! It looks like the pyjosev library is in two locations
# find / -name states.py
/ext/cache/cpm/josev/dd7dc8e95662d54aca01374e5283ead8d4793261/Josev/iso15118/shared/states.py
/ext/source/build/dist/libexec/everest/3rd_party/josev/iso15118/shared/states.py
I have been editing the one in build/dist
, but it may be that the code is getting pulled from the cache. Not sure how the original patch ever worked; maybe some parts are pulled from each location?!
Trying to modify an existing log to see which one is loaded (modifying the one in states.py
)
2024-07-16 19:24:00.442684 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: IN BUILD_DIST: Entered state ServiceDiscovery
2024-07-16 19:24:01.223707 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: IN BUILD_DIST: Entered state PaymentServiceSelection
2024-07-16 19:24:01.952899 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: IN BUILD_DIST: Entered state PaymentDetails
2024-07-16 19:24:02.769054 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: IN BUILD_DIST: Entered state Authorization
2024-07-16 19:24:03.475439 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: IN BUILD_DIST: Entered state ChargeParameterDiscovery
Let's make sure that the loggers are set up in the same way...
I might have made changes in the SECC implementation of pyjosev, which is unused in EVerest. The actual values are here: https://github.com/EVerest/ext-switchev-iso15118/blob/7f16c4b2c1307ce73798215f34b8fe06862bbae1/iso15118/evcc/controller/simulator.py#L276C15-L276C35
e_amount = PVEAmount(multiplier=3, value=60,
unit=UnitSymbol.WATT_HOURS)
ac_charge_params = ACEVChargeParameter(
departure_time=21600,
e_amount=e_amount,
ev_max_voltage=ev_max_voltage,
ev_max_current=ev_max_current,
ev_min_current=ev_min_current,
)
Bingo!
2024-07-16 19:33:51.232377 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: Message to encode (ns=urn:iso:15118:2:2013:MsgDef): {"V2G_Message": {"Header": {"SessionID": "BDF67F7EFD4FCEDD"}, "Body": {"ChargeParameterDiscoveryReq": {"RequestedEnergyTransferMode": "AC_three_phase_core", "AC_EVChargeParameter": {"DepartureTime": 21600, "EAmount": {"Value": 60, "Multiplier": 3, "Unit": "Wh"}, "EVMaxVoltage": {"Value": 400, "Multiplier": 0, "Unit": "V"}, "EVMaxCurrent": {"Value": 32000, "Multiplier": -3, "Unit": "A"}, "EVMinCurrent": {"Value": 10, "Multiplier": 0, "Unit": "A"}}}}}}
2024-07-16 19:33:51.944143 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: Decoded message (ns=urn:iso:15118:2:2013:MsgDef): {"V2G_Message":{"Header":{"SessionID":"BDF67F7EFD4FCEDD"},"Body":{"ChargeParameterDiscoveryRes":{"ResponseCode":"OK","EVSEProcessing":"Finished","SAScheduleList":{"SAScheduleTuple":[{"SAScheduleTupleID":1,"PMaxSchedule":{"PMaxScheduleEntry":[{"RelativeTimeInterval":{"start":0,"duration":86400},"PMax":{"Multiplier":0,"Unit":"W","Value":22080}}]}}]},"AC_EVSEChargeParameter":{"AC_EVSEStatus":{"NotificationMaxDelay":0,"EVSENotification":"None","RCD":false},"EVSENominalVoltage":{"Multiplier":-1,"Unit":"V","Value":2300},"EVSEMaxCurrent":{"Multiplier":-1,"Unit":"A","Value":320}}}}}}
Now, we set our charging profile
2024-07-16 19:37:02.768247 [INFO] ocpp:OCPP201 :: period.has_value() limit = 6900
2024-07-16 19:37:02.768315 [INFO] ocpp:OCPP201 :: period.has_value() stackLevel = 6900
2024-07-16 19:37:02.768429 [INFO] ocpp:OCPP201 :: ProfileId #1 Kind: Absolute
2024-07-16 19:37:02.768507 [INFO] ocpp:OCPP201 :: #1 find_period_at> 2024-07-16T19:00:00.000Z
2024-07-16 19:37:02.768611 [INFO] ocpp:OCPP201 :: find_period_at> start_time> 2024-07-16T19:37:02.768Z
2024-07-16 19:37:02.768690 [INFO] ocpp:OCPP201 :: find_period_at> period_start_time> 2024-07-16T19:00:00.000Z
2024-07-16 19:37:02.768808 [INFO] ocpp:OCPP201 :: find_period_at> period_end_time> 2024-07-16T20:00:00.000Z
2024-07-16 19:37:02.769048 [INFO] ocpp:OCPP201 :: PeriodDateTimePair> period: {
"limit": 10.0,
"numberPhases": 3,
"startPeriod": 0
} end_time: 2024-07-16T20:00:00.000Z
2024-07-16 19:37:02.769080 [INFO] ocpp:OCPP201 :: period.has_value() limit = 6900
2024-07-16 19:37:02.769131 [INFO] ocpp:OCPP201 :: period.has_value() stackLevel = 6900
And now we plug in the car, and we send the correct request, but the response still has only one SASchedule, which is 6900W for the entire day ("duration":86400).
2024-07-16 19:38:02.521698 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: Message to encode (ns=urn:iso:15118:2:2013:MsgDef): {"V2G_Message": {"Header": {"SessionID": "BFF27B0FED3BDBFE"}, "Body": {"ChargeParameterDiscoveryReq": {"RequestedEnergyTransferMode": "AC_three_phase_core", "AC_EVChargeParameter": {"DepartureTime": 21600, "EAmount": {"Value": 60, "Multiplier": 3, "Unit": "Wh"}, "EVMaxVoltage": {"Value": 400, "Multiplier": 0, "Unit": "V"}, "EVMaxCurrent": {"Value": 32000, "Multiplier": -3, "Unit": "A"}, "EVMinCurrent": {"Value": 10, "Multiplier": 0, "Unit": "A"}}}}}}
2024-07-16 19:38:03.085580 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: Sent ChargeParameterDiscoveryReq
2024-07-16 19:38:03.085726 [INFO] iso15118_charge :: Parameter-phase started
2024-07-16 19:38:03.086015 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: IN BUILD_DIST: Entered state ChargeParameterDiscovery
2024-07-16 19:38:03.088033 [INFO] iso15118_charge :: Selected energy transfer mode: AC_three_phase_core
2024-07-16 19:38:03.129469 [INFO] evse_manager_1: :: CAR ISO V2G ChargeParameterDiscoveryReq
2024-07-16 19:38:03.185479 [INFO] evse_manager_1: :: EVSE ISO V2G ChargeParameterDiscoveryRes
2024-07-16 19:38:03.240756 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: Decoded message (ns=urn:iso:15118:2:2013:MsgDef): {"V2G_Message":{"Header":{"SessionID":"BFF27B0FED3BDBFE"},"Body":{"ChargeParameterDiscoveryRes":{"ResponseCode":"OK","EVSEProcessing":"Finished","SAScheduleList":{"SAScheduleTuple":[{"SAScheduleTupleID":1,"PMaxSchedule":{"PMaxScheduleEntry":[{"RelativeTimeInterval":{"start":0,"duration":86400},"PMax":{"Multiplier":0,"Unit":"W","Value":6900}}]}}]},"AC_EVSEChargeParameter":{"AC_EVSEStatus":{"NotificationMaxDelay":0,"EVSENotification":"None","RCD":false},"EVSENominalVoltage":{"Multiplier":-1,"Unit":"V","Value":2300},"EVSEMaxCurrent":{"Multiplier":-1,"Unit":"A","Value":100}}}}}}
So it is fairly clear that the current ISO 15118-2 implementation does not work the way we had hoped in https://github.com/EVerest/everest-demo/issues/64#issue-2409427125
Concretely, the EVSE does not send a list of SASchedules; it only sends a single SASchedule that corresponds to the current limits. When the EVSE sent the ChargeParameterDiscoveryRes
, it basically said that you can get 6900 W for the rest of the day. This is technically correct because we started with the lowest current, but I don't think it will give us the energy we requested.
To run some numbers:
So basically, departure time does not work, and we only send a single SA Schedule as I had speculated from the code earlier. Not sure if it is super easy to fix this, but I have the afternoon to try!
Parsing the syntax & design behind the MQTT routes... My current understanding is that the DepartureTime should be send via the /carsim/cmd/{}
route, as the DepartureTime
is set by the EVCC (read: vehicle).
When looking at the docs for these routes (The (EVManager), we can see that there are several commands that are sent within a execute_charging_session
or modify_charging_session
message. As such, it doesn't seem appropriate to create an entirely new route; rather, I think the goal is to add a separate command within the execute_charging_session
packet. This goes back to my explorations earlier -- turns out I should be adding to the sim commands. I'm still a bit confused as to this process, but will do some digging into JsEvManager and EvManager's car_simulatorImpl.cpp. Should also probably take some time to investigate the testing code that covers these functions...
First, where is the limit computation coming from?
It is
int SmartChargingHandler::get_power_limit(const int limit, const int nr_phases,
const ChargingRateUnitEnum& unit_of_limit) {
if (unit_of_limit == ChargingRateUnitEnum::W) {
return limit;
} else {
return limit * LOW_VOLTAGE * nr_phases;
}
}
ah
const int LOW_VOLTAGE = 230;
updating the power limits in https://github.com/EVerest/everest-demo/issues/64#issuecomment-2231736589
Next question: why are we not handling the departure time correctly, and why are we returning exactly one SASchedule?
Confirmed that the return SASchedules (EVSE -> car) are created in the modules/EvseV2G/iso_server.cpp
, in handle_iso_charge_parameter_discovery
. We create one SASchedule with one PMax entry, and we set it to the entire day.
conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
.PMaxSchedule.PMaxScheduleEntry.array[0]
.RelativeTimeInterval.start = 0;
conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
.PMaxSchedule.PMaxScheduleEntry.array[0]
.RelativeTimeInterval.duration_isUsed = 1;
conn->ctx->evse_v2g_data.evse_sa_schedule_list.SAScheduleTuple.array[0]
.PMaxSchedule.PMaxScheduleEntry.array[0]
.RelativeTimeInterval.duration = SA_SCHEDULE_DURATION;
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;
So achieving our original goals, with multiple SASchedules is going to be fairly complex. Given the short timeframe, let us at least try for a much simpler option in with there is a single charging profile set, but the user specifies a departure time and energy need, and we spread the energy out over the entire charging period.
It seems that once you add a command to /carsim/cmd/{}
, it needs to be registered in both JsEvManager and EVManager's car_simulatorImpl.cpp.
So, we need to finally answer the question: How do we add commands to the message?
Looking at the Buffer sim commands function node's code:
if (msg.topic.indexOf('sim_commands') > -1) {
const s = msg.payload.split('#');
flow.set('sim_commands_start', s[0]);
flow.set('sim_commands_stop', s[1]);
flow.set('sim_commands_pause', s[2]);
flow.set('sim_commands_resume', s[3]);
}
// Code continues...
We can use the default injection to understand exactly how the payload is generated. The following string...
sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 36000#unplug#pause;sleep 3600#draw_power_regulated 16,3;sleep 36000
Assigns itself to each of the sim commands, as follows...
Flow Variable (Command) | Flow Value (Commands) |
---|---|
Start | sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 36000 |
Stop | unplug |
Pause | pause;sleep 3600 |
Resume | draw_power_regulated 16,3;sleep 36000 |
Ah, that makes more sense -- we set a series of commands for each possible method of interacting with the simulation loop! Cool. Know this, we finally have some steps to move forward: EDIT: Updating checklist.
Set DepartureTime
node should add a new command set_departure_time
to the Start and Resume phases of the simulation.
Ok, so I can confirm that I can read the values correctly in the ISO server code,
2024-07-16 23:30:07.580610 [WARN] iso15118_charge void dlog_func(dloglevel_t, const char*, int, const char*, const char*, ...) :: max_current 32.000000, nom_voltage 230, pmax 22080
2024-07-16 23:30:07.580705 [WARN] iso15118_charge void dlog_func(dloglevel_t, const char*, int, const char*, const char*, ...) :: Requested departure time 0, requested energy 60.000000
now to actually use the departure time and requested energy...
First design hiccup: We cannot simply pipe the set_departure_time
command into the buffer like the other commands. Consider the following scenario:
AC ISO 15118-2
AC ISO 15118-2: Plug & Charge
Solution: We still need have the set_departure_time
send its message through the buffer*, but we should also set a flag somewhere that the other that the car simulation node can check. I think this will be done via a "Flow Variable", as we have done is done with the sim_commands. Once I figure out how to set one of these scoped variables, this should be a relatively fix
*Caveat: I think this part of the function will behave quite similar to the simulation selection. That is, the change will be fed into the buffer, but the message will not propagate through to the mqtt server (This is because should only send departure time during a Start or Resume vent)
So I am not quite sure how we go from a complex set of schedule periods to a single limit.
OCPP calls OCPP201::set_external_limits
which passes in a composite schedule.
But when we get to the energy manager, we only return one optimized schedule
2024-07-16 23:59:55.150578 [INFO] energy_manager: :: Sending enfored limits (import) to :evse_manager_1 {
"ac_max_current_A": 32.0
}
2024-07-16 23:59:55.150671 [INFO] energy_manager: :: returning optimized values vector of length 1
2024-07-16 23:59:55.150816 [INFO] energy_manager: :: evse_manager_1 Enforce limits 32A -9999W
2024-07-16 23:59:55.277531 [ERRO] iso15118_charge void dlog_func(dloglevel_t, const char*, int, const char*, const char*, ...) :: In ISO15118 charger impl, after updating AC max current to 32.000000, we get 32.000000
If I can figure out how to pass this through, we might be able to do a better demo. Also checking to see whether the schedules are automatically recalculated (there is a timer that sets the limits)
Will we need a separate command to clear the departureTime once it's sent? Or will re-starting (resuming) without the command sufficient?
For now, let's assume we have to delete the command. With some initial futzing, this has proven to be somewhat non trivial. Below is my exploration of this first approach.
Well, after drafting that up, I think it's time to take a step back: we can do better. Instead of removing or adding the commands (which starts to add complications), what happens if we simply mutate the value within the command? Let's re-do the truth tables from my first attempt
departure_time_msg has value | departure_time_msg empty | |
---|---|---|
departure_time_cmd in start/resume | Update current commands to new value | Update current commands to new value |
N/A | N/A |
Update Simulation Profile | |
---|---|
departure_time_cmd defined | Update Command |
departure_time_cmd undefined | Update Command |
Phew!! I was really gilding the lily with the first approach -- this is way simpler.
To summarize: Going forward, each "simulation profile" will have a iso_set_departure_time
command. This command will be one of two values: null, or a 32u value. This means that the signal being sent here is not in absolute parity with the spec, but this approach (i) allows us to include / exclude the extension on the EVManager, and (ii) greatly simplifies the UI code.
Yay! with some fairly obvious changes, this now works!
2024-07-17 00:41:59.842439 [WARN] iso15118_charge void dlog_func(dloglevel_t, const char*, int, const char*, const char*, ...) :: before adjusting for departure time, max_current 32.000000, nom_voltage 230, pmax 22080
2024-07-17 00:41:59.842500 [WARN] iso15118_charge void dlog_func(dloglevel_t, const char*, int, const char*, const char*, ...) :: Requested departure time 21600, requested energy 60000.000000
2024-07-17 00:41:59.842541 [WARN] iso15118_charge void dlog_func(dloglevel_t, const char*, int, const char*, const char*, ...) :: Min hours to charge 2.717391, requested departure time in hours 6.000000, plenty of time to charge, lowering pmax = 1.000000
2024-07-17 00:41:59.843014 [ERRO] iso15118_charge void dlog_func(dloglevel_t, const char*, int, const char*, const char*, ...) :: CHECKME: when is this called and do we need to modify it as well
2024-07-17 00:41:59.925920 [INFO] evse_manager_1: :: CAR ISO V2G ChargeParameterDiscoveryReq
2024-07-17 00:41:59.981000 [INFO] evse_manager_1: :: EVSE ISO V2G ChargeParameterDiscoveryRes
2024-07-17 00:41:59.990256 [DEBG] iso15118_car pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: Decoded message (ns=urn:iso:15118:2:2013:MsgDef): {"V2G_Message":{"Header":{"SessionID":"3968BF77DD9EBDB9"},"Body":{"ChargeParameterDiscoveryRes":{"ResponseCode":"OK","EVSEProcessing":"Finished","SAScheduleList":{"SAScheduleTuple":[{"SAScheduleTupleID":1,"PMaxSchedule":{"PMaxScheduleEntry":[{"RelativeTimeInterval":{"start":0,"duration":21600},"PMax":{"Multiplier":0,"Unit":"W","Value":10000}}]}}]},"AC_EVSEChargeParameter":{"AC_EVSEStatus":{"NotificationMaxDelay":0,"EVSENotification":"None","RCD":false},"EVSENominalVoltage":{"Multiplier":-1,"Unit":"V","Value":2300},"EVSEMaxCurrent":{"Multiplier":-1,"Unit":"A","Value":320}}}}}}
However, the EVSEMaxCurrent
needs to be fixed, in the second part of the handler. Not quite sure why we are handling it twice, but whatever...
Hm, more hiccups -- it doesn't seem to be as simple as "slotting in a new command", I think I'm missing some key aspect of Node-RED. When adding the command with the following code...
function handleDepartureTime() {
const cmd = flow.get('sim_departure_time');
let start = flow.get('sim_commands_start').split(';');
let resume = flow.get('sim_commands_resume').split(';');
start.splice(3, 1, cmd);
resume.splice(3, 1, cmd);
flow.set('sim_comands_start', start.join())
flow.set('sim_commands_resume', resume.join())
}
if (msg.topic.indexOf('sim_commands') > -1) {
const s = msg.payload.split('#');
flow.set('sim_commands_start', s[0]);
flow.set('sim_commands_stop', s[1]);
flow.set('sim_commands_pause', s[2]);
flow.set('sim_commands_resume', s[3]);
handleDepartureTime();
} else if (msg.topic == 'departure_time') {
cmd = 'iso_set_departure_time ' + msg.payload
flow.set('sim_departure_time', cmd);
handleDepartureTimeUpdate();
} else if (msg.payload == 'start') {
// Code Continues...
And updating the charging profiles accordingly, it appears that the flow contexts are not being properly set:
What gives? Clearly I'm missing something -- the ",ac" is being interpreted into a full string, and `flow.set('sim_departure_time') does not initialize the context.
Reading the docs of nodered, I was assuming the addition of a context should be as simple as .set()
. It seems there is some initialization step that I've missed, as all 4 contexts are populated upon a fresh deployment. This doesn't seem to occur within the initialize Connector
nodes -- I guess I need to do more reading...
Well, that's an embarrassing typo to miss -- it helps if you spell your context variable names correctly š¤¦ Issue seems to be that I misspelled the start
command. Making some tweaks now, will update accordingly.
Woohoo!! The UI is finally tee'ing up the commands properly. Below is a video of the functionality, and the JSON file of the flow diagram.
https://github.com/user-attachments/assets/6f67939e-2040-4fcb-b03c-e980fc8b5966
Great! @the-bay-kay now I think you just need to set it in the connection context (similar to the payment mode) and then populate the departure time in PyJosev from that instead of hardcoding it to 0 in build/dist/libexec/everest/3rd_party/josev/iso15118/evcc/controller/simulator.py
line 302.
I am fully done with my changes, and they appear to work. We cannot demo what we had originally planned to demo, but we can demo something interesting with departure times. I'm going to edit the main description here (https://github.com/EVerest/everest-demo/issues/64#issue-2409427125) to outline our plans for the this demo and create a follow-up issue for the more complex demo in the future.
EDIT: created issue https://github.com/EVerest/everest-demo/issues/68
Alright! Time to start setting the commands in context. Since we snuck the command into the start
and resume
commands sequences, we luckily don't have to edit any of the mqtt routes. So, let's start (somewhat arbitrarily) with JsEvManager.
First things first, I think we can ignore the corresponding mainfest.yaml file, as nothing here pertains to the departureTime changes. Then, we need to look at the index.js. Within this file, it appears there's a few functions we'll need to edit:
mod
variable's parameter that contains the 32u (default null
, to reflect the lack of extension). Likewise, the registerAllCommands()
function should be mutating whatever variable we set here, correct...? For now, let's call this value mod.iso_departure_time
.mod.iso_departure_time
.iso_set_departure_time
), I think the c.args[] array should be empty as a result. Thus, we can check the size of the array, and only set the departure time if len(c.args) > 0. sleep
command), but worth mentioning that this is something that occurs.It seems like those are the only two places where this file'll need to be changed. The changes themselves seem simple (I already have them mocked up), but I have to go back and tweak the UI so that the base case (no input) passes the length(c.args)
check. Once these are written, I need to see where the value should be used in the JsEvManager simulation loop
Finally figured out how to check for a non-input. I expected "null", or "", but you had to leave the check entirely blank. Now that this is sorted, we have a default value of "null", which can then be parsed in JsEvManager
's index.js
. Woot!
Cool! I think we can now check off the two boxes. We can incorporate the new command as follows...
Next task is figuring out where we need to use this new mod.iso_departure_time
variable!
Checking through all the use cases in https://github.com/EVerest/everest-demo/issues/64#issue-2409427125 before creating the patches and rebuilding the image...
@the-bay-kay at worst, I can demo tomorrow by editing the python file!
at worst, I can demo tomorrow by editing the python file!
Oh wonderful! That's good to know at least -- I'll still try to push to get the UI connected, it'd be cool to see the full thing finished this afternoon : )
@the-bay-kay Absolutely! There will be other opportunities to demo the more polished version, including maybe at the JO All Hands!
You should also plumb through the EAmount (either after the departure time or in parallel, depending on whatever is easier). The request is really the combination of the two - think of it in terms of phone charging if you don't have experience with EV charging: "I'm here for 4 hours and I want to fill in enough charge to get to 80%".
"I'm here for 4 hours" alone is not as useful; do you want to just top up a bit before you go home, or do you have plans for a long subway ride and want to make sure that you will be able to listen to music the whole way?
Ok! Coffee's made, back to it:
@the-bay-kay
But great progress so far, and š¤ that this is wrapped up soon!
When adding EAmount:
Wh / Watt-Hour
afteriso_set_dtea
, for nowiso_set_dtea departureTime EAmount
EDIT: Some more notes on EAmount:
@shankari , quick clarifying question: Under the hood, the EAmount represents a number of Watt-Hours. Setting this by hand seems rather clunky -- should we make the input a percentage instead? We could then calculate the Watt-Hours needed to reach that percentage on the back-end (though I may need some help with that process!). Because of [V2G 742], we will already need to do some calculations to figure out the WH left in a given charge resumption, so this may be something we want to tack on. If we want completely granular control, however, I can leave the input as the raw EAmount.
We can make the input a %, but then we would need to also allow the user to specify the battery capacity. In cars, similar to phones, you can have a wide variety of battery capacities.
What would be really cool as a future change, IMHO, would be to find a mapping between car (make, model year) and battery capacity (ideally through an API) and then allow users to specify that and the %.
Actually, I think we can do that with the EPA database if we specify miles. EPA has an MPGe or similar rating for electric cars. If we can hook into that, we can allow users to specify make, model, year and miles needed. We should then be able to calculate MPGe and then convert that back to energy. And honestly that is what makes the most sense from a user perspective - I want to make sure that I have enough miles to get to my destination.
https://en.wikipedia.org/wiki/Miles_per_gallon_gasoline_equivalent
That would be cool! Makes sense -- for now, I'll stick with the raw EAmount for ease of implementation,
(Though ease may be the word, I don't know why setting a flow context is so finicky... Definitely still missing something lol)
OK -- I had to use change
nodes to set flow contexts (I couldn't just set them from the JS, which seems bizarre to me -- everything I've read said you could do so without any initialization.....), so we've got both signal values being set now.
For bookkeeping, here's the JSON file: flow_w_eadt.json
Let's try to build on #44 to include departure time.
EDIT: Simplified scenarios with a single SASchedule. More complex schedules are tracked in #68
High level scenario using AC L2:
[1] This is a real-life example from our recent trip to Arcata. We got to the hotel at around 8pm with a Tesla that was almost empty. We wanted to walk to a concert the next morning (9am - 11am) and then leave at noon rather than deal with parking at the concert venue. The hotel had validated parking in the public lot, but the charger was not smart, and there was no communication between the network and the hotel - the validated permit was a piece of paper to put on the windshield. So we got a notification at around 6am indicating that we would start getting charged for parking since the charging was complete. In this case, the fast charging was actually bad - my husband had to run down to the lot and move the car right after he woke up.