1010#include < ArduinoOcpp.h> // Facade for ArduinoOcpp
1111#include < ArduinoOcpp/SimpleOcppOperationFactory.h> // define behavior for incoming req messages
1212
13+ #include " http_update.h"
14+
1315#include < ArduinoOcpp/Core/OcppEngine.h>
1416
1517#include " emonesp.h" // for VOLTAGE_DEFAULT
@@ -28,10 +30,11 @@ ArduinoOcppTask::~ArduinoOcppTask() {
2830 instance = NULL ;
2931}
3032
31- void ArduinoOcppTask::begin (EvseManager &evse, LcdTask &lcd) {
33+ void ArduinoOcppTask::begin (EvseManager &evse, LcdTask &lcd, EventLog &eventLog ) {
3234
3335 this ->evse = &evse;
3436 this ->lcd = &lcd;
37+ this ->eventLog = &eventLog;
3538
3639 initializeArduinoOcpp ();
3740 loadEvseBehavior ();
@@ -60,7 +63,20 @@ void ArduinoOcppTask::initializeArduinoOcpp() {
6063
6164 OCPP_initialize (ocppSocket, (float ) VOLTAGE_DEFAULT, ArduinoOcpp::FilesystemOpt::Use, clockAdapter);
6265
63- bootNotification (" Advanced Series" , " OpenEVSE" , [this ](JsonObject payload) {
66+ initializeDiagnosticsService ();
67+ initializeFwService ();
68+
69+ DynamicJsonDocument *evseDetailsDoc = new DynamicJsonDocument (JSON_OBJECT_SIZE (6 ));
70+ JsonObject evseDetails = evseDetailsDoc->to <JsonObject>();
71+ evseDetails[" chargePointModel" ] = " Advanced Series" ;
72+ // evseDetails["chargePointSerialNumber"] = "TODO"; //see https://github.com/OpenEVSE/ESP32_WiFi_V4.x/issues/218
73+ evseDetails[" chargePointVendor" ] = " OpenEVSE" ;
74+ evseDetails[" firmwareVersion" ] = evse->getFirmwareVersion ();
75+ // evseDetails["meterSerialNumber"] = "TODO";
76+ // evseDetails["meterType"] = "TODO";
77+ // see https://github.com/OpenEVSE/ESP32_WiFi_V4.x/issues/219
78+
79+ bootNotification (evseDetailsDoc, [this ](JsonObject payload) { // ArduinoOcpp will delete evseDetailsDoc
6480 LCD_DISPLAY (" OCPP connected!" );
6581 });
6682
@@ -87,14 +103,21 @@ void ArduinoOcppTask::loadEvseBehavior() {
87103 });
88104
89105 setEnergyActiveImportSampler ([this ] () {
90- return (float ) evse->getTotalEnergy ();
106+ float activeImport = 0 .f ;
107+ activeImport += (float ) evse->getTotalEnergy ();
108+ activeImport += (float ) evse->getSessionEnergy ();
109+ return activeImport;
91110 });
92111
93112 setOnChargingRateLimitChange ([this ] (float limit) { // limit = maximum charge rate in Watts
94113 charging_limit = limit;
95114 this ->updateEvseClaim ();
96115 });
97116
117+ setConnectorPluggedSampler ([this ] () {
118+ return (bool ) evse->isConnected ();
119+ });
120+
98121 setEvRequestsEnergySampler ([this ] () {
99122 return (bool ) evse->isCharging ();
100123 });
@@ -103,12 +126,47 @@ void ArduinoOcppTask::loadEvseBehavior() {
103126 return evse->isActive ();
104127 });
105128
129+ /*
130+ * Report failures to central system. Note that the error codes are standardized in OCPP
131+ */
132+
133+ addConnectorErrorCodeSampler ([this ] () {
134+ if (evse->getEvseState () == OPENEVSE_STATE_GFI_FAULT ||
135+ evse->getEvseState () == OPENEVSE_STATE_NO_EARTH_GROUND ||
136+ evse->getEvseState () == OPENEVSE_STATE_DIODE_CHECK_FAILED) {
137+ return " GroundFailure" ;
138+ }
139+ return (const char *) NULL ;
140+ });
141+
142+ addConnectorErrorCodeSampler ([this ] () {
143+ if (evse->getEvseState () == OPENEVSE_STATE_OVER_TEMPERATURE) {
144+ return " HighTemperature" ;
145+ }
146+ return (const char *) NULL ;
147+ });
148+
149+ addConnectorErrorCodeSampler ([this ] () {
150+ if (evse->getEvseState () == OPENEVSE_STATE_OVER_CURRENT) {
151+ return " OverCurrentFailure" ;
152+ }
153+ return (const char *) NULL ;
154+ });
155+
156+ addConnectorErrorCodeSampler ([this ] () {
157+ if (evse->getEvseState () == OPENEVSE_STATE_STUCK_RELAY ||
158+ evse->getEvseState () == OPENEVSE_STATE_GFI_SELF_TEST_FAILED) {
159+ return " InternalError" ;
160+ }
161+ return (const char *) NULL ;
162+ });
163+
106164 /*
107165 * CP behavior definition: How will plugging and unplugging the EV start or stop OCPP transactions
108166 */
109167
110168 onVehicleConnect = [this ] () {
111- if (getTransactionId () < 0 ) {
169+ if (getTransactionId () < 0 && isAvailable () ) {
112170 if (!ocpp_idTag.isEmpty ()) {
113171 authorize (ocpp_idTag, [this ] (JsonObject payload) {
114172 if (idTagIsAccepted (payload)) {
@@ -170,6 +228,24 @@ void ArduinoOcppTask::loadEvseBehavior() {
170228 this ->updateEvseClaim ();
171229 });
172230
231+ setOnResetReceiveReq ([this ] (JsonObject payload) {
232+ const char *type = payload[" type" ] | " Soft" ;
233+ if (!strcmp (type, " Hard" )) {
234+ resetHard = true ;
235+ }
236+
237+ resetTime = millis ();
238+ resetTriggered = true ;
239+
240+ LCD_DISPLAY (" Reboot EVSE" );
241+ });
242+
243+ setOnUnlockConnector ([] () {
244+ // TODO Send unlock command to peripherals. If successful, return true, otherwise false
245+ // see https://github.com/OpenEVSE/ESP32_WiFi_V4.x/issues/230
246+ return false ;
247+ });
248+
173249 updateEvseClaim ();
174250}
175251
@@ -207,6 +283,19 @@ unsigned long ArduinoOcppTask::loop(MicroTasks::WakeReason reason) {
207283 onVehicleDisconnect ();
208284 }
209285
286+ if (resetTriggered) {
287+ if (millis () - resetTime >= 10000UL ) { // wait for 10 seconds after reset command to send the conf msg
288+ resetTriggered = false ; // execute only once
289+
290+ if (resetHard) {
291+ // TODO send reset command to all peripherals
292+ // see https://github.com/OpenEVSE/ESP32_WiFi_V4.x/issues/228
293+ }
294+
295+ restart_system ();
296+ }
297+ }
298+
210299 return 0 ;
211300}
212301
@@ -282,11 +371,11 @@ String ArduinoOcppTask::getCentralSystemUrl() {
282371 if (url.isEmpty ()) {
283372 return url; // return empty String
284373 }
285- if (!url.endsWith (" /" )) {
286- url += ' /' ;
287- }
288374 String chargeBoxId = ocpp_chargeBoxId;
289375 chargeBoxId.trim ();
376+ if (!url.endsWith (" /" ) && !chargeBoxId.isEmpty ()) {
377+ url += ' /' ;
378+ }
290379 url += chargeBoxId;
291380
292381 if (MongooseOcppSocketClient::isValidUrl (url.c_str ())) {
@@ -318,6 +407,164 @@ void ArduinoOcppTask::reconfigure() {
318407 loadEvseBehavior ();
319408}
320409
410+ void ArduinoOcppTask::initializeDiagnosticsService () {
411+ ArduinoOcpp::DiagnosticsService *diagService = ArduinoOcpp::getDiagnosticsService ();
412+ if (diagService) {
413+ diagService->setOnUploadStatusSampler ([this ] () {
414+ if (diagFailure) {
415+ return ArduinoOcpp::UploadStatus::UploadFailed;
416+ } else if (diagSuccess) {
417+ return ArduinoOcpp::UploadStatus::Uploaded;
418+ } else {
419+ return ArduinoOcpp::UploadStatus::NotUploaded;
420+ }
421+ });
422+
423+ diagService->setOnUpload ([this ] (String &location, ArduinoOcpp::OcppTimestamp &startTime, ArduinoOcpp::OcppTimestamp &stopTime) {
424+
425+ // reset reported state
426+ diagSuccess = false ;
427+ diagFailure = false ;
428+
429+ // check if input URL is valid
430+ unsigned int port_i = 0 ;
431+ struct mg_str scheme, query, fragment;
432+ if (mg_parse_uri (mg_mk_str (location.c_str ()), &scheme, NULL , NULL , &port_i, NULL , &query, &fragment)) {
433+ DBUG (F (" [ocpp] Diagnostics upload, invalid URL: " ));
434+ DBUGLN (location);
435+ diagFailure = true ;
436+ return false ;
437+ }
438+
439+ if (eventLog == NULL ) {
440+ diagFailure = true ;
441+ return false ;
442+ }
443+
444+ // create file to upload
445+ #define BOUNDARY_STRING " -----------------------------WebKitFormBoundary7MA4YWxkTrZu0gW025636501"
446+ const char *bodyPrefix PROGMEM = BOUNDARY_STRING " \r\n "
447+ " Content-Disposition: form-data; name=\" file\" ; filename=\" diagnostics.log\"\r\n "
448+ " Content-Type: application/octet-stream\r\n\r\n " ;
449+ const char *bodySuffix PROGMEM = " \r\n\r\n " BOUNDARY_STRING " --\r\n " ;
450+ const char *overflowMsg PROGMEM = " {\" diagnosticsMsg\" :\" requested search period exceeds maximum diagnostics upload size\" }" ;
451+
452+ const size_t MAX_BODY_SIZE = 10000 ; // limit length of message
453+ String body = String (' \0 ' );
454+ body.reserve (MAX_BODY_SIZE);
455+ body += bodyPrefix;
456+ body += " [" ;
457+ const size_t SUFFIX_RESERVED_AREA = MAX_BODY_SIZE - strlen (bodySuffix) - strlen (overflowMsg) - 2 ;
458+
459+ bool firstEntry = true ;
460+ bool overflow = false ;
461+ for (uint32_t i = 0 ; i <= (eventLog->getMaxIndex () - eventLog->getMinIndex ()) && !overflow; i++) {
462+ uint32_t index = eventLog->getMinIndex () + i;
463+
464+ eventLog->enumerate(index, [this , startTime, stopTime, &body, SUFFIX_RESERVED_AREA, &firstEntry, &overflow] (String time, EventType type, const String &logEntry, EvseState managerState, uint8_t evseState, uint32_t evseFlags, uint32_t pilot, double energy, uint32_t elapsed, double temperature, double temperatureMax, uint8_t divertMode) {
465+ if (overflow) return ;
466+ ArduinoOcpp::OcppTimestamp timestamp = ArduinoOcpp::OcppTimestamp ();
467+ if (!timestamp.setTime (time.c_str ())) {
468+ DBUG (F (" [ocpp] Diagnostics upload, cannot parse timestamp format: " ));
469+ DBUGLN (time);
470+ return ;
471+ }
472+
473+ if (timestamp < startTime || timestamp > stopTime) {
474+ return ;
475+ }
476+
477+ if (body.length () + logEntry.length () + 10 < SUFFIX_RESERVED_AREA) {
478+ if (firstEntry)
479+ firstEntry = false ;
480+ else
481+ body += " ," ;
482+
483+ body += logEntry;
484+ body += " \n " ;
485+ } else {
486+ overflow = true ;
487+ return ;
488+ }
489+ });
490+ }
491+
492+ if (overflow) {
493+ if (!firstEntry)
494+ body += " ,\r\n " ;
495+ body += overflowMsg;
496+ }
497+
498+ body += " ]" ;
499+
500+ body += bodySuffix;
501+
502+ DBUG (F (" [ocpp] POST diagnostics file to " ));
503+ DBUGLN (location);
504+
505+ MongooseHttpClientRequest *request =
506+ diagClient.beginRequest (location.c_str ());
507+ request->setMethod (HTTP_POST);
508+ request->addHeader (" Content-Type" , " multipart/form-data; boundary=" BOUNDARY_STRING);
509+ request->setContent (body.c_str ());
510+ request->onResponse ([this ] (MongooseHttpClientResponse *response) {
511+ if (response->respCode () == 200 ) {
512+ diagSuccess = true ;
513+ } else {
514+ diagFailure = true ;
515+ }
516+ });
517+ request->onClose ([this ] () {
518+ if (!diagSuccess) {
519+ // triggered onClose before onResponse
520+ diagFailure = true ;
521+ }
522+ });
523+ diagClient.send (request);
524+
525+ return true ;
526+ });
527+ }
528+ }
529+
530+ void ArduinoOcppTask::initializeFwService () {
531+ ArduinoOcpp::FirmwareService *fwService = ArduinoOcpp::getFirmwareService ();
532+ if (fwService) {
533+ fwService->setBuildNumber (evse->getFirmwareVersion ());
534+
535+ fwService->setInstallationStatusSampler ([this ] () {
536+ if (updateFailure) {
537+ return ArduinoOcpp::InstallationStatus::InstallationFailed;
538+ } else if (updateSuccess) {
539+ return ArduinoOcpp::InstallationStatus::Installed;
540+ } else {
541+ return ArduinoOcpp::InstallationStatus::NotInstalled;
542+ }
543+ });
544+
545+ fwService->setOnInstall ([this ](String &location) {
546+
547+ DBUGLN (F (" [ocpp] Starting installation routine" ));
548+
549+ // reset reported state
550+ updateFailure = false ;
551+ updateSuccess = false ;
552+
553+ return http_update_from_url (location, [] (size_t complete, size_t total) { },
554+ [this ] (int status_code) {
555+ // onSuccess
556+ updateSuccess = true ;
557+
558+ resetTime = millis ();
559+ resetTriggered = true ;
560+ }, [this ] (int error_code) {
561+ // onFailure
562+ updateFailure = true ;
563+ });
564+ });
565+ }
566+ }
567+
321568bool ArduinoOcppTask::operationIsAccepted (JsonObject payload) {
322569 const char *status = payload[" status" ] | " Invalid" ;
323570 return !strcmp (status, " Accepted" );
0 commit comments