Closed shankari closed 2 months ago
@drmrd @the-bay-kay @giavotto @louisg1337 based on the discussion today, I think that this is the list of questions that we need to answer before the demo. At least a one of them will result in fallback plans, and I think there are at least two tasks for the OCPP smart charging implementation in here. Please let me know if I missed anything, and we can divvy up the tasks between NREL and AFS
To figure out whether we can do this demo, we need to determine the following:
If Departure Time is omitted and the EVSE is attempting to run a smart charging session according to K15, the EVSE should provide a schedule greater than or equal to 24 hours.
Check out [V2G2-304]: If the EVCC did not provide a DepartureTime Target Setting (refer to subclause 8.4.3.8.2 and 8.5.3.2) in the ChargeParameterDiscoveryReq message, the sum of the individual time intervals described in the PMaxSchedule and SalesTariff provided in the ChargeParameterDiscoveryRes message, shall be greater or equal to 24 hours.
I read V2G2-303, should have gone on to V2G2-304 😄
It looks like ISO 151180-2 is at least sending the correct messages - I see a ChargeParameterDiscovery Req/Res pair
2024-05-16 14:58:32.319347 [INFO] evse_manager_1: :: CAR ISO V2GChargeParameterDiscoveryReq
2024-05-16 14:58:32.376123 [INFO] evse_manager_1: :: EVSE ISO V2G ChargeParameterDiscoveryRes
2024-05-23 17:38:26.930783 [INFO] evse_manager_1: :: CAR ISO V2G PowerDeliveryReq
2024-05-23 17:38:26.973277 [INFO] evse_manager_1: :: EVSE ISO V2G PowerDeliveryRes
To see what is in the message, we can look at the code, or pull in the EXI message logging from the ISO 15118-2 DC demo, or see if we can enable EXI logging/decoding through EVerest @louisg1337
On the OCPP side, though, it looks like the sequence is
Receive V2GChargeParameterDiscoveryReq
Send TransactionEvent with "eventType": "Started" and a bunch of meter values
Send TransactionEvent with "eventType": "Updated" and a charging state of "Charging"
There is no NotifyXXX message sent, and no ChargingProfile set from the CSMS.
@drmrd @Giavotto @louisg1337 One potential scaled back but hacked together demo might be:
V2GChargeParameterDiscoveryReq
V2GChargeParameterDiscoveryRes
as planned yesterday@louisg1337 can you figure out what is in the current EXI messages for V2GChargeParameterDiscoveryReq
and V2GChargeParameterDiscoveryRes
and what else we can send?
@shankari will send out a message to Citrine about their smart charging support and potential for hacking
The current EXI messages are logged at /tmp/everest-logs/2024-05-23.../incomplete-eventlog.html
on the manager.
I found a decoder online (http://exificient.github.io/javascript/) but it requires a grammar. @drmrd I know that, when we were doing the demos to stakeholders last year, you used an EXI decoder. Can you provide instructions on what it is, and how to use it to decode messages? I don't think we documented it at the time.
OCPP 2.0.1
In the repo it says that 2.0.1 is fully implemented and that they aren't maintaining 2.0.1 anymore and are instead focusing on developing 2.1. It does seem a bit unclear though if that is the case. I keep on finding places where things are defined, like SetChargingProfile
or Notify***
but I can't seem to find where else they are used.
OCPP 2.1 I'm not sure if we want to use the newer version but it seems like stuff is actually being used here.
Even though CitrineOS lacks some features that we want, they at least have some way to set a charging profile and it seems like it can be set using a REST API.
@louisg1337 couple of notes:
Actually, I take my comments about Citrine back - it looks like it does expose SetChargingProfile
via the swagger API (@louisg1337 let's prioritize confirming that). Per the citrine-core README,
@AsMessageEndpoint to expose functions allowing to send messages to charging stations
@AsMessageEndpoint(
CallAction.SetChargingProfile,
SetChargingProfileRequestSchema,
)
setChargingProfile(
identifier: string,
tenantId: string,
request: SetChargingProfileRequest,
callbackUrl?: string,
): Promise<IMessageConfirmation> {
return this._module.sendCall(
identifier,
tenantId,
CallAction.SetChargingProfile,
request,
callbackUrl,
);
}
However, the implementation still seems to be a NOP - I don't see anything being done there other than logging the message. It would be good to test out invoking calls from that endpoint and see if it actually makes it to the station.
We are going to try to see if the calls are sent to the station. However, citrine currently doesn't support PnC (related PR: https://github.com/citrineos/citrineos-core/pull/100). However, it also doesn't appear to support security profile 3
8081: websocket server tcp connection without auth
8082: websocket server tcp connection with basic http auth
I don't see any mention of a TLS connection; only regular TCP. This means that EVerest has to communicate with it using security profile 1. In SP1, everest is configured to connect to port 80 with the password DEADBEEFDEADBEEF
. So if we remap the basic HTTP auth port in citrine (8082) to port 80 (https://github.com/citrineos/citrineos-core/blob/63670f3adc09266a0977862d972b0f7e440c577f/Server/docker-compose.yml#L96) and add the station with the correct password, we should be able to connect and test out the setChargingProfile command.
Since CitrineOS doesn't support SP3, and the open PR doesn't seem to add it either, briefly looking at what it would take to add this in MaEVe. It doesn't look too bad. I assume we are going for:
reconfigure
in the manager API as an example, add calls to manager/api/api-spec.yaml
and manager/api/server.go
and store the value when received)NotifyEvChargingNeedsRequest
(look at TransactionEvent
or BootNotification
as an example, add a new route in manager/handlers/ocpp201/routing.go
and a new handler in manager/handlers/ocpp201
, similar to `, process the event by sending a response and triggering the next step)setChargingProfileRequest
(look at TriggerMessage
as an example)To have the NotifyEvChargingNeedsRequest
trigger the setChargingProfileRequest
, I think we need to just send an MQTT message to the broker - examples of how to do that are in the manager/handlers/ocpp201/routing_test.go
I checked the logs and can verify that the messages from the charging station are being received through MQTT (more formally Charge Station -> websocket -> gateway -> mqtt -> manager). Checking TransactionEventHandler
, it is pretty clear that the typical structure is for the handlers to call services and then send out a response. I am not sure what the planned structure is for dependencies between messages (e.g. should one handler call another handler or call a service), but I am pretty sure that for the current hacking, we can just send out two messages from the handler and it will work.
From @louisg1337,
The websocket server is using security profiles 1 or 2 and the request’s Authorization header has an incorrect username or password. The username will be checked against the charging station’s id as set in the url it connected with. The password will be checked against the device model associated with the station id. Specifically, it will be the Actual Variable Attribute’s value that belongs to the
BasicAuthPassword
Variable on theSecurityCtrlr
Component. You can set this VariableAttribute on the CSMS side using the Variable Attribute CRUD endpoints on the Monitoring module. You can set this VariableAttribute on the Charging Station side using the SetVariables message, which can be sent from CitrineOS using the Monitoring module’s message API.
When looking at the API endpoints (http://localhost:8080/docs/ if you have Citrine running) he didn't see BasicAuthPassword anywhere.
Further updates about this in the PR https://github.com/EVerest/everest-demo/pull/45
I have the station connecting to the CSMS, but can't figure out how to add auth tokens. Once those are added, we should be able to charge. @louisg1337 can you try to build on this demo by setting the charging profile on the station? If it is easy, you could also figure out how to actually add new tokens to the CSMS.
After that, thought, we should switch to MaEVe since I am not sure if Citrine supports SP3
@shankari CitrineOS has supported SP3 since the first full release in January, since SP3 is a part of the Advanced Security use cases. The System Configuration object contains the fields needed to provide keys & certificates for tls/mtls. We added additional documentation to help describe these fields, which you can find here.
If you're having difficulty figuring out how CitrineOS supports a use case from one of its currently supported certification profiles, please reach out on Github or Discord. CitrineOS currently supports Core; Advanced Security; Advanced User Interface; Advanced Device Management; and, with the open PRs you linked, ISO15118.
@thanaParis thank you for the quick response. It is great to hear that Citrine supports SP3. I think that what we are struggling with is that the documentation is not very clear, at least compared to MaEVe, and given the short time frame, we need to make a decision on which CSMS we should try to integrate with.
For example, you say that CitrineOS supports SP3, but the documentation just says the following:
CitrineOS (service name: citrineos) with ports
8080: webserver http - [Swagger](http://localhost:8080/docs)
8081: websocket server tcp connection without auth
8082: websocket server tcp connection with basic http auth
which port should we use for SP3? It is not clear. For the record, I tried to connect to both 8081 and 8082 using ws://
and was successful (8081 added the station, 8082 failed with a 401 error as expected since we weren't able to figure out how to set the password). So it is not clear what the wss://
port even is.
In contrast, MaEVe's documentation says:
Charge stations can connect to the CSMS using:
ws://localhost/ws/<cs-id>
wss://localhost/ws/<cs-id>
Even with the documentation that you linked to, it is not clear what we need to do to enable SP3.
Host: The hostname for the WebSocket server.
Port: The port number for the WebSocket server.
Ping Interval: The interval in seconds between pings to keep the connection alive.
Protocol: The communication protocol used.
What do you mean by "the hostname for the websocket server"? You will presumably listen to 0.0.0.0, and the websocket connection will be initiated by the client running on the station. So which server hostname do we need to specify?
We understand that we can get help on Discord or on GitHub, but they are slower - we have to formulate the question and wait for an answer. It is a lot easier if we can use the documentation directly at least for these basic steps and then reach out to the Discord/GitHub for more complex changes.
To streamline this process, would you be open to modifying the "single line demos" that we have in this repo to showcase how to connect via SP1, SP2 and SP3, similar to https://github.com/EVerest/everest-demo/pull/45? We have the certificates created, and they work with MaEVe - you can run curl https://raw.githubusercontent.com/everest/everest-demo/main/demo-iso15118-2-ac-plus-ocpp.sh | bash -s -- -3
on a computer that has docker installed, go to http://localhost:1880/ui and initiate a charge session.
If you could get Citrine to the same level, we could figure out what changes we need for the smart charging demo.
@louisg1337 wrt looking at the ISO 15118-2 messages, I don't know if you saw https://lists.lfenergy.org/g/everest/message/1538 which we can use in our containerized environment using https://superuser.com/questions/1685605/how-to-capture-docker-container-traffic-using-wireshark
@louisg1337, great job on #45! While we working on review and merge that PR, and @thanaParis and CitrineOS take a look at the issue with the tokens, I would like to return to setChargingProfile
REST API call.
https://github.com/EVerest/everest-demo/issues/44#issuecomment-2129978468 https://github.com/EVerest/everest-demo/issues/44#issuecomment-2130071076
Can you try to invoke setChargingProfile
via a REST API call (either through Swagger or through a script or both!) on CitrineOS, and see what it does? Concretely, does it actually pass the call through the websocket interface to EVerest, and does EVerest receive it successfully? If you do get it to work using a script, can you create a new PR that adds that script to this repo?
Exploring https://github.com/EVerest/everest-demo/issues/44#issuecomment-2127744537 further
can you figure out what is in the current EXI messages for V2GChargeParameterDiscoveryReq and V2GChargeParameterDiscoveryRes and what else we can send?
I followed this through to the source code. The car simulator requires iso15118_ev
(and slac...).
provides:
main:
interface: car_simulator
description: This implements the car simulator
requires:
simulation_control:
interface: yeti_simulation_control
ev:
interface: ISO15118_ev
min_connections: 0
max_connections: 1
slac:
interface: slac
min_connections: 0
max_connections: 1
The iso15118_ev
interface is implemented by (at least) PyJosev
$ grep -r --before-context=2 ISO15118_ev modules
modules/PyEvJosev/manifest.yaml-provides:
modules/PyEvJosev/manifest.yaml- ev:
modules/PyEvJosev/manifest.yaml: interface: ISO15118_ev
--
modules/simulation/JsCarSimulator/manifest.yaml- interface: yeti_simulation_control
modules/simulation/JsCarSimulator/manifest.yaml- ev:
modules/simulation/JsCarSimulator/manifest.yaml: interface: ISO15118_ev
or
provides:
ev:
interface: ISO15118_ev
description: This module implements the ISO15118-2 implementation of an EV
pyjosev includes the python libraries from https://github.com/EVerest/ext-switchev-iso15118 Notably, here https://github.com/EVerest/ext-switchev-iso15118/blob/6aaaacf690008217bd094447d91c2a723c6261ee/iso15118/evcc/controller/simulator.py#L304
we ask for 1 WH with a departure time of 0 and a bunch of other statically configured values. If we wanted to, I am pretty sure we can hack this to specify a departure time as well. However, it is not clear that the departure time is in fact necessary for the demo.
Although it looks like there are other, more complex schedules defined here as well with departure times and targetSoC. https://github.com/EVerest/ext-switchev-iso15118/blob/6aaaacf690008217bd094447d91c2a723c6261ee/iso15118/evcc/controller/simulator.py#L421 or https://github.com/EVerest/ext-switchev-iso15118/blob/6aaaacf690008217bd094447d91c2a723c6261ee/iso15118/evcc/controller/simulator.py#L435
Next steps for mucking around with the EV SIL:
The more complex schedules are for -20 - they create ScheduledScheduleExchangeReqParams
messages, which are not defined in -2. Let's trace out how the calls get passed through from the simulator and see if we can create a new command for the new schedules.
Here's the call stack:
modules/simulation/JsCarSimulator
has several "commands" which call methods on the modules that they use
So the car simulator module https://github.com/EVerest/everest-demo/issues/44#issuecomment-2143307139
uses slac
and ev
and implements commands like registerCmd(mod, 'iso_wait_slac_matched', 0, (mod, c) => {
mod.state = 'pluggedin';
if (mod.slac_state === undefined) return false;
if (mod.slac_state === 'UNMATCHED') if (mod.uses_list.slac.length > 0) mod.uses_list.slac[0].call.enter_bcd();
if (mod.slac_state === 'MATCHED') return true;
});
// --- wip
start_charging
, and it is implemented in PyJosev
https://github.com/EVerest/everest-core/blob/54ea32be8432d9292879ed0f36e181a6b37e3266/modules/PyEvJosev/module.py#L96 def _handler_start_charging(self, args) -> bool:
self._es.PaymentOption =args['PaymentOption']
self._es.EnergyTransferMode = args['EnergyTransferMode']
self._ready_event.set()
return True
DCEVChargeParameter
The PyJosev
module also supports a call to set EV parameters
def _handler_set_dc_params(self, args):
parameters = args['EV_Parameters']
self._es.dc_max_current_limit = parameters['MaxCurrentLimit']
self._es.dc_max_power_limit = parameters['MaxPowerLimit']
self._es.dc_max_voltage_limit = parameters['MaxVoltageLimit']
self._es.dc_energy_capacity = parameters['EnergyCapacity']
self._es.dc_target_current = parameters['TargetCurrent']
self._es.dc_target_voltage = parameters['TargetVoltage']
which is called right after we register the commands
registerAllCmds(mod);
mod.enabled = false;
if (mod.uses_list.ev.length > 0) mod.uses_list.ev[0].call.set_dc_params(get_hlc_dc_parameters(mod));
and the simulator reads the values from the state
max_current_limit_value, max_current_limit_multiplier = float2Value_Multiplier(
EVEREST_EV_STATE.dc_max_current_limit)
max_power_limit_value, max_power_limit_multiplier = float2Value_Multiplier(
EVEREST_EV_STATE.dc_max_power_limit)
max_voltage_limit_value, max_voltage_limit_multiplier = float2Value_Multiplier(
EVEREST_EV_STATE.dc_max_voltage_limit)
...
self.dc_ev_charge_params: DCEVChargeParams = DCEVChargeParams(
dc_max_current_limit=PVEVMaxCurrentLimit(
multiplier=max_current_limit_multiplier, value=max_current_limit_value, unit=UnitSymbol.AMPERE
),
and uses it to
dc_charge_params = DCEVChargeParameter(
departure_time=0,
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,
ev_maximum_voltage_limit=self.dc_ev_charge_params.dc_max_voltage_limit,
ev_energy_capacity=self.dc_ev_charge_params.dc_energy_capacity,
ev_energy_request=ev_energy_request,
full_soc=100,
bulk_soc=80,
)
So for the SIL demo, I think we can do the following:
ChargeParameterDiscoveryReq
with an energy request of 6000 WH and max values as follows (current: 300, voltage: 900, power: 150000)ChargeParameterDiscoveryRes
with an SASchedule that provides the max power requestedChargeParameterDiscoveryRes
that provides 900 V @ 10amps instead of 300 amps.JsCarSimulator
to PyJosev
to the iSO15118
EVCC simulator by adding new arguments to the V2G commandWe should then be able to experiment with other scenarios around power requests and/or departure times
wrt viewing the messages so that we can see what is being sent and received, we cannot use wireshark just yet because the communication is between two modules in the same manager code. However, we can get the EXI messages using the logs and try to decode them. From https://github.com/EVerest/everest-demo/issues/44#issuecomment-2127954519, there is an online decoder or we can use cbexigen
(https://github.com/EVerest/cbexigen) or even what is currently used (https://github.com/chargebyte/openv2g).
At least for cbiexigen and the online decoder, we need the schemas:
In order to be able to produce a codec, the standard's XML schema files are required. These cannot be distributed with the code generator. They are available within the actual standard documents, partly distributed separately by the ISO, and also available openly from other sources.
EDIT: there are schemas here https://github.com/FlUxIuS/V2Gdecoder/tree/master/schemas. Notably
@drmrd bumping this up again to see if you can provide a link to the EXI decode tool that you used in the initial set of demos.
Can you try to invoke
setChargingProfile
via a REST API call (either through Swagger or through a script or both!) on CitrineOS, and see what it does? Concretely, does it actually pass the call through the websocket interface to EVerest, and does EVerest receive it successfully? If you do get it to work using a script, can you create a new PR that adds that script to this repo?
Unfortunately it seems like setChargingProfile
is indeed a NOP and doesn't send anything to EVerest. I used the Swagger API and made a few requests to all of the x-ChargingProfile
commands and they all returned this error within the server-citrine-1
log in Docker.
2024-06-02 17:47:32 2024-06-02 21:47:32.842 ERROR /usr/local/apps/citrineos/03_Modules/OcppRouter/dist/module/router.js:496 CitrineOS Logger:MessageRouterImpl Failed processing call error:
2024-06-02 17:47:32
2024-06-02 17:47:32 Error Method not implemented.
2024-06-02 17:47:32 error stack:
2024-06-02 17:47:32 • router.js MessageRouterImpl.<anonymous>
2024-06-02 17:47:32 /usr/local/apps/citrineos/03_Modules/OcppRouter/dist/module/router.js:595
2024-06-02 17:47:32 •
2024-06-02 17:47:32
2024-06-02 17:47:32 • router.js
2024-06-02 17:47:32 /usr/local/apps/citrineos/03_Modules/OcppRouter/dist/module/router.js:12
2024-06-02 17:47:32 •
2024-06-02 17:47:32
2024-06-02 17:47:32 • router.js __awaiter
2024-06-02 17:47:32 /usr/local/apps/citrineos/03_Modules/OcppRouter/dist/module/router.js:8
2024-06-02 17:47:32 • router.js MessageRouterImpl._routeCallError
2024-06-02 17:47:32 /usr/local/apps/citrineos/03_Modules/OcppRouter/dist/module/router.js:575
2024-06-02 17:47:32 • router.js
2024-06-02 17:47:32 /usr/local/apps/citrineos/03_Modules/OcppRouter/dist/module/router.js:487
2024-06-02 17:47:32 • task_queues process.processTicksAndRejections
2024-06-02 17:47:32 internal/process/task_queues:95
Looking at the errors specifically, Failed processing call error
shows up here, and Method not implemented
can be found here, and it seems like they are both dead ends. There is, however, a callbackUrl
parameter that we can pass to the API request, and that gets invoked a few lines above the Method not implemented
error here. Not sure if this is possible/how it works, but maybe we can reroute a message to our x-ChargingProfile
endpoint in EVerest using this?
@louisg1337 the Failed processing call error
is called from _onCallError
so we are already in the error processing. _onCallError
is invoked from here. The actual call is invoked here. Are you sure that this is a NOP and that the call was not passed through to EVerest? Note that EVerest almost certainly doesn't have setChargingProfile
implemented yet.
Can you provide additional details? How did you invoke the swagger call? What were the logs on Citrine? What were the logs on EVerest? Did you check the OCCP message logs on EVerest? If you add additional logs to sendCall
- are you able to see the message and the response?
Following up on https://github.com/EVerest/everest-demo/issues/44#issuecomment-2143965306, I am able to decode most messages after dropping the initial bytes based on https://sourceforge.net/p/openv2g/discussion/1226276/thread/e4bca31da7/ I noticed that the example in V2GDecoder.jar
begins with 8098, so I tried to decode the EXI beginning at that point (so if the log has 01fe8001000000208098023b66b9df9bfffb8f9094400006301e0188120060018407d00818601400
, we decode 8098023b66b9df9bfffb8f9094400006301e0188120060018407d00818601400
). With that, I am able to decode most messages from an ISO 15118-2 charge session EXCEPT the following messages: SessionSetupRes
, ServiceDiscoveryRes
, PaymentServiceSelectionReq
, PaymentServiceSelectionRes
, ChargeParameterDiscoveryRes
which return null
.
I have attached the eventLog HTML here and the decoded outputs for the main V2G messages.
the Failed processing call error is called from _onCallError so we are already in the error processing.
This was definitely an oversight on my part, thanks for catching that for me. I took some time to go back and trace what happens from when we call setChargingProfile()
and it backs up the fact that we successfully send a message to EVerest.
Tracing it from the start...
sendCall()
.sendCall()
.{ success: true }
response when sending the call, so I am assuming that this line returns true, and we successfully send the message to EVerest.EDIT:
I am a bit confused now because it seems like the above should be the general flow of trying to do setChargingProfile()
. However, when I was trying to add in more logs to debug further, I realized none of the logs were actually from the functions above. Maybe this is because we are manipulating things through an API, so we skip any abstraction work that the functions above may do. Here is the new trace of what happens when we send the API request...
send()
gets called which is apart of the rabbit-mq sender code._onMessage()
, which is apart of the rabbit-mq receiver code.
_onMessage()
, it passes the message along to this.handle()
, which is then probably what ends up calling this.sendCallError()
, starting our Error message chain. I do want to verify this with some further logging, however, once I get that to work (more on logging later in this post). How did you invoke the swagger call?
I invoked it via the API ui on localhost:8080/docs, then navigated to the POST
request for setChargingProfile
. For the identifier field I put in cp001
and tenantId I put in louis
. The whole CURL request is as follows.
What were the logs on Citrine?
What were the logs on EVerest? / Did you check the OCCP message logs on EVerest?
In the command line that is running the SIL simulation nothing shows up whenever I send the setChargingProfile
API request. I'm not sure if there is a different place I am supposed to be looking for logs as well, if so, please let me know!
If you add additional logs to
sendCall
- are you able to see the message and the response?
I've been trying to add logs to Citrine but I just can't seem to get the logs to show in the server-citrine-1
logs section. I put some logs in the handle
function in AbstractRouter
, right next to the Received message: Message {
log that we get above, but my logs don't show. I made all the changes in my forked repo here, and I have been redirecting the bash script to point to my citrine repo instead using this code:
c) CSMS="citrine"
CSMS_REPO="https://github.com/louisg1337/citrineos-core"
CSMS_BRANCH="test" ;;
Any ideas on how to get these logs to show would be appreciated as it would help us dig a bit deeper.
Any ideas on how to get these logs to show would be appreciated as it would help us dig a bit deeper.
Can you look at the logs that do show up, and how they are generated? If you push your test branch, I can also take a look tonight.
The logs are being generated using tslog
and are invoked using this._logger.debug/error(...args)
. I am not sure how that translates to the Logs tab in the server-citrine-1
docker container though, and if there is an extra step between tslog
and the Logs tab that I am missing.
My CitrineOS test branch is pushed and has all the logs I've been testing. I've been noticing though that even when I try to change the wording of logs we currently see, Method not implemented
and Received message
in particular, the changes still aren't even showing. Maybe theres a possibility that I am still copying over the same original repo or there is some type of caching happening that prevents me from seeing these changes.
To reproduce, I pushed my newly updated demo script to my citrine_demo
branch, which incorporates the CitrineOS log changes I have been trying. Running the demo in that branch should recreate what I am experiencing above.
@louisg1337 this may be non-obvious because you are running this through a script, but docker-compose up -d
doesn't rebuild the images. You need to manually build first. If I run the script from your repo, the checked out version is
Resolving deltas: 100% (5046/5046), done.
...
HEAD is now at 1b26f2c Changed another word to see if the changes actually do anything
And the containers start
[+] Running 5/5
✔ Network server_default Created 0.1s
✔ Container server-ocpp-db-1 Healthy 0.1s
✔ Container server-amqp-broker-1 Healthy 0.1s
✔ Container server-directus-1 Healthy 0.1s
✔ Container server-citrine-1 Started 15.5s
Waiting 5s for CSMS to start...
But the images were built 7 days ago
server-citrine latest 3b38adb4231f 7 days ago 517MB
server-directus latest 6ee9bfbd8b11 7 days ago 807MB
If I add a build step
diff --git a/demo-iso15118-2-ac-plus-ocpp.sh b/demo-iso15118-2-ac-plus-ocpp.sh
index 115b251..8ee463b 100755
--- a/demo-iso15118-2-ac-plus-ocpp.sh
+++ b/demo-iso15118-2-ac-plus-ocpp.sh
@@ -176,6 +176,7 @@ if [[ "$DEMO_VERSION" != v1.6j ]]; then
fi
fi
+ docker compose build
docker compose up -d
We rebuild before starting
Starting the CSMS
Replaced mapping CitrineOS 8082 to 80 completed successfully.
WARN[0000] /var/folders/y5/cx3cfzrd2q116myv9ly86sw1rnlmdj/T/tmp.GIByGpUd/citrine-csms/Server/docker-compose.yml: `version` is obsolete
[+] Building 37.2s (22/26) docker:desktop-linux
=> [directus] exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:6ee9bfbd8b11c8551d6a6a1fcfec870a6bc64c73f30df25ca1454e612315526e 0.0s
=> => naming to docker.io/library/server-directus 0.0s
=> [citrine internal] load build definition from deploy.Dockerfile 0.1s
=> => transferring dockerfile: 591B 0.0s
=> [citrine internal] load metadata for docker.io/library/node:18-slim 0.7s
=> [citrine internal] load metadata for docker.io/library/node:18 0.7s
=> [citrine auth] library/node:pull token for registry-1.docker.io 0.0s
=> [citrine internal] load .dockerignore 0.0s
=> => transferring context: 439B 0.0s
=> [citrine build 1/5] FROM docker.io/library/node:18@sha256:b08b1356559e2e9945f47ded630c9eb9d4e3ca04f1 0.0s
=> [citrine stage-1 1/3] FROM docker.io/library/node:18-slim@sha256:1da7652745e9ba5de396e436aa086588ea5 0.0s
=> [citrine internal] load build context 0.1s
=> => transferring context: 4.85MB 0.1s
=> CACHED [citrine build 2/5] WORKDIR /usr/local/apps/citrineos 0.0s
=> [citrine build 3/5] COPY . . 0.2s
=> [citrine build 4/5] RUN npm install --workspaces --verbose && npm run compile --workspaces --verbos 34.4s
=> => # npm http fetch GET 200 https://registry.npmjs.org/@sideway%2faddress 160ms (cache miss)
=> => # npm http fetch GET 200 https://registry.npmjs.org/@sideway%2fpinpoint 179ms (cache miss)
=> => # npm http fetch GET 200 https://registry.npmjs.org/uri-js 92ms (cache miss)
=> => # npm http fetch GET 200 https://registry.npmjs.org/content-disposition 97ms (cache miss)
=> => # npm http fetch GET 200 https://registry.npmjs.org/p-limit 109ms (cache miss)
=> => # npm http fetch GET 200 https://registry.npmjs.org/@fastify%2faccept-negotiator 455ms (cache miss)
@drmrd @couryrr-afs @wjmp had a conversation today about what we plan to support. Given the short timeframe, we plan to support a very simple version of what is already the most basic use case:
Beyond this basic scenario, we can experiment with complex composite schedules that will be generated using the composite schedule tool validation tool. We will not be adding support for NotifyEVChargingNeedsRequest/Response
and the transaction level charging profile at this time.
Note that I am not 100% sure if this meets the OCPP 2.0.1 standard; the standard clearly indicates that async charging profiles are supported (from section 3.1,
Outside the context of a transaction as a separate message to set a charging profile to a local controller, Charging Station, or a default charging profile to an EVSE.)
But if there is an "out of band" charging profile, will the charge station not send a NotifyEVChargingNeedsRequest
when the EV plugs in? Or is that only for "load leveling"? The problem is that the OCPP spec refers to ISO 15118-1, not -2, so it is hard to figure out what it is referring to. Will double-check at the next working group.
We've created a fork which supports SP 1 and SP 2 for CitrineOS
Example usage:
./demo-iso15118-2-ac-plus-ocpp.sh -2 -c -r $(pwd)
This will start CitrineOS alongside EVerest on SP 2.
There is an outstanding issue with SP 3 that I haven't been able to pin down. We've connected chargers to CitrineOS via SP 3 in the past at OCA Plugfests, but were not able to replicate those successes with EVerest. Previously, I thought this was because CitrineOS didn't use intermediary certificates, but we now support those and it still doesn't work. The everest-demo SP 3 setup works with MaEVe, so I hunted for differences between our handshakes with openssl s_client -connect
, but was unable to find anything of note. I will continue to look into this.
My CitrineOS test branch is pushed and has all the logs I've been testing. I've been noticing though that even when I try to change the wording of logs we currently see, Method not implemented and Received message in particular, the changes still aren't even showing. Maybe theres a possibility that I am still copying over the same original repo or there is some type of caching happening that prevents me from seeing these changes.
If you are using docker to run Citrine, which I highly recommend, you do need to remember to execute docker compose down
between builds--simply docker compose build && docker compose up -d
will often reuse the last run image rather than the latest.
@thanaParis great to see the changes in the fork; can you submit it as a PR that I can review and merge? Note the changes to the run script location were a regression that I fixed in https://github.com/EVerest/everest-demo/pull/49
so I hunted for differences between our handshakes with openssl s_client -connect, but was unable to find anything of note. I will continue to look into this.
wireshark might help. When we got SP3 working with MaEVe, I recall it being fairly useful. there are standard methods to use wireshark to inspect docker networks; we should really enable that by default. @the-bay-kay, did you get wireshark to work when debugging the EonTI PKI?
At least for cbiexigen and the online decoder, we need the schemas:
In order to be able to produce a codec, the standard's XML schema files are required. These cannot be distributed with the code generator. They are available within the actual standard documents, partly distributed separately by the ISO, and also available openly from other sources.
EDIT: there are schemas here https://github.com/FlUxIuS/V2Gdecoder/tree/master/schemas. Notably
The ISO -2 and -20 schemas can be obtained here: https://standards.iso.org/iso/15118/
The DIN schemas in V2Gdecoder are outdated and broken. Check this location instead: https://github.com/SwitchEV/iso15118/tree/master/iso15118/shared/schemas
setChargingProfile
Update
Thank you @shankari and @thanaParis for the help with figuring out my docker issues I had above. Now that I was able to add additional logs, I was able to clear up some of my confusion and I think it is safe to say that setChargingProfile
does in fact send a message to EVerest. Please correct me if I'm wrong, but it seems like the general flow once sending an POST request to setChargingProfile
is as follows...
sender.ts / send()
receiver.ts / _onMessage()
handle()
AbstractRouter.ts / handle()
sendCall()
router.ts / sendCall()
... wait for reply ...
router.ts / onMessage()
_onCallError()
router.ts / _onCallError()
onMessage
should only get called if Citrine receives any messages, so I think it is safe to say that Citrine successfully communicates with EVerest.
@louisg1337 I think it would be worthwhile to fix the logging so that it is clear. We shouldn't expect that everybody pokes through the code to understand which layer has not implemented the function. Isn't it possible to change the error to say something like "Received from charge station: <error>"
🚧 Will mark this at TODO for now. 🚧 ; we should file a separate issue in Citrine and potentially contribute a fix
Thanks to @barsnick, after adding some additional zeros at the end of the EXI message (now it is 8098023b66b9df9bfffb8f90a000000000002028c140c50c0ac010a800000042000000000000
), I am able to decode the ChargeParameterDiscoveryRes
Field | Req | Res |
---|---|---|
Departure time | 0 | 86400 (24 60 60) |
Energy/power | 60 Wh | 22080 W |
Voltage | 400 V (max) | $0 \times 10^{-1}$ V (nominal) |
Current | $32000 \times 10^{-3} A = 32 A max, 10 A min | $0 \times 10^{-3}$ h |
It is not clear why the EVSE is giving 22080 W at 0V and 0A. The 0,0
may be a decoding error after adding on zeros. But it is still not clear why the energy/power is off by so much.
wireshark might help. When we got SP3 working with MaEVe, I recall it being fairly useful. there are standard methods to use wireshark to inspect docker networks; we should really enable that by default. ... did you get wireshark to work when debugging the EonTI PKI?
@shankari I've had somewhat mixed results -- I've got the linuxserver wireshark container (docs) running, but there seem to be some issues with its interraction with WSL-2. Below are some notes on the setup, and where how I'm working to fix it and get it running!
Because I do not currently have access to my personal linux machine, and the demos do not work with my NREL laptop's M1-Chip (link), I'm running the docker image with the following setup on another personal computer:
With this, I have successfully been able to spin up the cert demos with wireshark running in the background...
But, when I attempt to connect to the wireshark application (docs) via 127.0.0.1:3000, 3001, or any similar port / url combo, it fails to connect...
When inspecting the docker container, we get the following error: Of note is the line nl80211 not found
. Looking at this github thread, it seems I'm not the first person who's had this issue with WSL-2, and the best workarounds seem rather complex (link).
It seems that, like with the M1 Chip, there's some inherent issues to the WSL approach -- adding wireshark appears to work otherwise! I'm currently getting VMWare Workstation 17 set up on this machine, and will report back once I run tests on a full Linux machine. For those curious, the wireshark addition to docker-compose.ocpp201.yml
is as simple as the following (with more complex options found here)
services:
# Other servces ...
wireshark:
image: lscr.io/linuxserver/wireshark:latest
container_name: wireshark
But, when I attempt to connect to the wireshark application (docs) via 127.0.0.1:3000, 3001, or any similar port / url combo, it fails to connect...
You haven't exposed the appropriate port from the wireshark container.
<ns6:EVSEMaxCurrent> <ns6:Multiplier>-3</ns6:Multiplier><ns6:Unit>h</ns6:Unit><ns6:Value>0</ns6:Value> </ns6:EVSEMaxCurrent>
Current shouldn't have the unit "hours". 😉 This (and the 10^-3) is a side effect of filling the truncated EXI message with zeros for it to decode without errors, In reality, some other values must have been truncated.
Before going a lot further, let's test the other combinations (AC ISO 15118 + EIM, DC ISO 15118 + EIM and DC ISO 15118 + PnC). We will need to use at least the first two to test the HIL since we are not sure how to install certs on the vector ECCU.
API Request
The goal we had in mind was to see whether or not calling the triggerChargeStation
API actually sent something to EVerest. The first step was to send an API request and check the OCPP logs to see if our request showed up. I sent the API request below, and observed the following in the picture further down.
curl -X POST \
'http://localhost:9410/api/v0/cs/cp001/trigger' \
-H 'Content-Type: application/json' \
-d '{
"trigger": "SignCombinedCertificate"
}'
I have not observed SignCombinedCertificate
get sent out by the system before, and that time was around the time I sent it, so I think it was reasonable to say that the triggerChargeStation
API reaches EVerest.
Code Tracing
After observing the above, I also wanted to trace through the MaEVe code to get a better understanding of how the triggerChargeStation
API call actually gets sent to EVerest. The below demonstrates the general flow of how the trigger call works its way through the code.
API calls tracing, specifically triggers...
manager / api / server.go ... TriggerChargeStation()
(link)
TriggerChargeStation()
we save the trigger to the store
err := s.store.SetChargeStationTriggerMessage(r.Context(), csId, &store.ChargeStationTriggerMessage{
TriggerMessage: store.TriggerMessage(req.Trigger),
TriggerStatus: store.TriggerStatusPending,
})
manager / sync / triggers.go ... SyncTriggers()
(link)
runEvery
time, check to see if any new triggers, if so then send()
err = v201CallMaker.Send(ctx, csId, &ocpp201.TriggerMessageRequestJson{
RequestedMessage: ocpp201.MessageTriggerEnumType(pendingTriggerMessage.TriggerMessage),
})
manager / handlers / call_maker.go ... Send()
, physically sends the message to EVerest (link)Note
When I was tracing the code, the "calls made internally" section is how I originally thought triggerChargeStation
was handled, but once I added in logs, I realized that wasn't the case. I kept the internal call section below just to document in case we ever needed to understand this part of the codebase better.
Calls made internally (i.e. BootNotification
, GetCertificateStatus
)
manager / handlers / router.go ... route()
(link)
route, ok := r.CallRoutes[message.Action]
manager/handlers/ocpp201/routing.go ... NewRouter()
(link)resp, err := route.Handler.HandleCall(ctx, chargeStationId, req)
manager / handlers / occp201 / xxx.go
there are a ton of different functionalities all of which have their own HandleCall
functionssetChargingProfile
ExplorationDiving deeper into the MaEVe exploration, I tried to see if setChargingProfile
was supported at all. Unfortunately, it looks like this feature hasn't been developed yet as the below bullet points are all I have found that related to it. I looked in the repo for everything related to setChargingProfile
, chargingProfile
, charging
, and profile
. I also did a bit of research online to see if there was an active branch that had these changes in the works or if someone had their own forked version with it, but I didn't find either.
manager / ocpi / server.go ... x-ChargingProfile()
functions (link)
w.WriteHeader(http.StatusNotImplemented)
manager / schemas / ocpp201 ... x-ChargingProfileRequest.json / x-ChargingProfileResponse.json
json
schema files, not used anywheremanager / ocpp / ocpp201 / request_start_transaction_request.go ... type ChargingProfileType struct
(link)
@louisg1337 yes, setChargingProfile
is not currently supported on MaEVe. So we want to add support for it, plumbing it through to the charge station. You should use triggerChargeStation
as an example and follow the flow that you discovered in https://github.com/EVerest/everest-demo/issues/44#issuecomment-2150985185
To follow up on https://github.com/EVerest/everest-demo/issues/44#issuecomment-2150633370, I tried adding a new manager, hooking up the second connector to it, and expanding the options in the node-red dropdown. The new config and new node-red are attached.
active_modules:
iso15118_charger:
module: EvseV2G
config_module:
device: auto
tls_security: allow
verify_contract_cert_chain: false
connections:
security:
- module_id: evse_security
implementation_id: main
iso15118_car:
module: PyEvJosev
config_module:
device: auto
supported_ISO15118_2: true
tls_active: true
is_cert_install_needed: false
evse_manager_1:
module: EvseManager
config_module:
connector_id: 1
three_phases: true
has_ventilation: true
country_code: DE
evse_id: "DE*PNX*00001"
session_logging: true
session_logging_xml: false
session_logging_path: /tmp/everest-logs
charge_mode: AC
ac_hlc_enabled: true
ac_hlc_use_5percent: false
ac_enforce_hlc: false
connections:
bsp:
- module_id: yeti_driver_1
implementation_id: board_support
powermeter_grid_side:
- module_id: yeti_driver_1
implementation_id: powermeter
slac:
- module_id: slac
implementation_id: evse
hlc:
- module_id: iso15118_charger
implementation_id: charger
evse_manager_2:
module: EvseManager
config_module:
connector_id: 2
country_code: DE
evse_id: "DE*PNX*E12345*2"
session_logging: true
session_logging_xml: false
session_logging_path: /tmp/everest-logs
charge_mode: DC
hack_allow_bpt_with_iso2: true
connections:
bsp:
- module_id: yeti_driver_2
implementation_id: board_support
powermeter_car_side:
- module_id: powersupply_dc
implementation_id: powermeter
slac:
- module_id: slac
implementation_id: evse
hlc:
- module_id: iso15118_charger
implementation_id: charger
powersupply_DC:
- module_id: powersupply_dc
implementation_id: main
imd:
- module_id: imd
implementation_id: main
powersupply_dc:
module: JsDCSupplySimulator
yeti_driver_1:
module: JsYetiSimulator
config_module:
connector_id: 1
yeti_driver_2:
module: JsYetiSimulator
config_module:
connector_id: 2
slac:
module: JsSlacSimulator
imd:
module: IMDSimulator
car_simulator_1:
module: JsCarSimulator
config_module:
connector_id: 1
auto_enable: true
auto_exec: false
auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug
connections:
simulation_control:
- module_id: yeti_driver_1
implementation_id: yeti_simulation_control
ev:
- module_id: iso15118_car
implementation_id: ev
slac:
- module_id: slac
implementation_id: ev
car_simulator_2:
module: JsCarSimulator
config_module:
connector_id: 2
auto_enable: true
auto_exec: false
auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug
dc_target_current: 20
dc_target_voltage: 400
connections:
simulation_control:
- module_id: yeti_driver_2
implementation_id: yeti_simulation_control
ev:
- module_id: iso15118_car
implementation_id: ev
slac:
- module_id: slac
implementation_id: ev
ocpp:
module: OCPP201
connections:
evse_manager:
- module_id: evse_manager_1
implementation_id: evse
- module_id: evse_manager_2
implementation_id: evse
auth:
- module_id: auth
implementation_id: main
system:
- module_id: system
implementation_id: main
security:
- module_id: evse_security
implementation_id: main
evse_security:
module: EvseSecurity
config_module:
private_key_password: "123456"
token_provider_1:
module: DummyTokenProviderManual
auth:
module: Auth
config_module:
connection_timeout: 120
selection_algorithm: PlugEvents
connections:
token_provider:
- module_id: token_provider_1
implementation_id: main
- module_id: ocpp
implementation_id: auth_provider
- module_id: evse_manager_1
implementation_id: token_provider
- module_id: evse_manager_2
implementation_id: token_provider
token_validator:
- module_id: ocpp
implementation_id: auth_validator
evse_manager:
- module_id: evse_manager_1
implementation_id: evse
- module_id: evse_manager_2
implementation_id: evse
energy_manager:
module: EnergyManager
connections:
energy_trunk:
- module_id: grid_connection_point
implementation_id: energy_grid
grid_connection_point:
module: EnergyNode
config_module:
fuse_limit_A: 40.0
phase_count: 3
connections:
price_information: []
energy_consumer:
- module_id: evse_manager_1
implementation_id: energy_grid
- module_id: evse_manager_2
implementation_id: energy_grid
powermeter:
- module_id: yeti_driver_1
implementation_id: powermeter
api:
module: API
connections:
evse_manager:
- module_id: evse_manager_1
implementation_id: evse
system:
module: System
x-module-layout: {}
However, several of the combinations failed with weird errors. For example, the AC EIM test failed because the manager reported that only the DC_Extended
Energy Transfer mode was working although it is should be connected to evse_manager_1, which has AC power configured.
We need to investigate this further, but for now, since the HIL demo runs on hardware with a single port , let's have stwo separate configs (AC and DC) and two separate nodered files (AC and DC).
For the record, we have also now plumbed through setChargingProfile support in MaEVe so that we can showcase PnC with SmartCharging (at least in the SIL, and even in the HIL if we can get the certs configured correctly). https://github.com/EVerest/everest-demo/pull/55
And have cleaned up the demo to support both PnC and EIM with ISO 15118. The NREL HIL we will use only supports EIM, and this also gives us flexibility during the testival. Both Louis and I have verified that we can run it successfully to the level that the charge session starts; setChargingProfile, of course, is still unimplemented. https://github.com/EVerest/everest-demo/pull/57#issuecomment-2157051709
In a few short weeks, we are going to try to pull together a demo for CharIN, to be held in Cleveland from June 11-14. Given that we have been working on smart charging recently, the goal is to try and demo basic, end to end smart charging.
So this flow diagram: