Skip to content

Commit 4c93993

Browse files
authored
Merge pull request #79 from basmeerman/work/plan-03
feat: OCPP robustness — LB exclusivity, FreeVend guard, settings validation, telemetry
2 parents ec11dcb + d65278f commit 4c93993

File tree

12 files changed

+601
-9
lines changed

12 files changed

+601
-9
lines changed

SmartEVSE-3/src/esp32.cpp

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ char RequiredEVCCID[32] = ""; // R
6060
#include <MicroOcppMongooseClient.h>
6161
#include <MicroOcpp/Core/Configuration.h>
6262
#include <MicroOcpp/Core/Context.h>
63+
#include "ocpp_logic.h"
64+
#include "ocpp_telemetry.h"
6365
#endif //ENABLE_OCPP
6466

6567
#if SMARTEVSE_VERSION >= 40
@@ -354,6 +356,8 @@ extern uint8_t OcppTrackCPvoltage;
354356
extern MicroOcpp::MOcppMongooseClient *OcppWsClient;
355357

356358
extern float OcppCurrentLimit;
359+
extern bool OcppWasStandalone;
360+
extern ocpp_telemetry_t OcppTelemetry;
357361

358362
extern unsigned long OcppStopReadingSyncTime; // Stop value synchronization: delay StopTransaction by a few seconds so it reports an accurate energy reading
359363

@@ -1151,6 +1155,18 @@ void mqttPublishData() {
11511155
#if ENABLE_OCPP && defined(SMARTEVSE_VERSION) //run OCPP only on ESP32
11521156
mqtt_pub_str(MQTT_SLOT_OCPP, "/OCPP", OcppMode ? "Enabled" : "Disabled", true, now_s);
11531157
mqtt_pub_str(MQTT_SLOT_OCPP_CONNECTION, "/OCPPConnection", (OcppWsClient && OcppWsClient->isConnected()) ? "Connected" : "Disconnected", false, now_s);
1158+
mqtt_pub_str(MQTT_SLOT_OCPP_TX_ACTIVE, "/OCPPTxActive", OcppTelemetry.tx_active ? "true" : "false", false, now_s);
1159+
{
1160+
char ocpp_limit_buf[16];
1161+
if (OcppCurrentLimit >= 0.0f) {
1162+
snprintf(ocpp_limit_buf, sizeof(ocpp_limit_buf), "%.1f", (double)OcppCurrentLimit);
1163+
} else {
1164+
snprintf(ocpp_limit_buf, sizeof(ocpp_limit_buf), "none");
1165+
}
1166+
mqtt_pub_str(MQTT_SLOT_OCPP_CURRENT_LIMIT, "/OCPPCurrentLimit", ocpp_limit_buf, false, now_s);
1167+
}
1168+
mqtt_pub_str(MQTT_SLOT_OCPP_SMART_CHARGING, "/OCPPSmartCharging",
1169+
OcppTelemetry.lb_conflict ? "Conflict" : (!LoadBl ? "Active" : "Inactive"), false, now_s);
11541170
#endif //ENABLE_OCPP
11551171
{ // LED color topics — build string in buffer
11561172
char color_buf[16];
@@ -1697,6 +1713,8 @@ bool ocppLockingTxDefined() {
16971713

16981714
void ocppInit() {
16991715

1716+
ocpp_telemetry_init(&OcppTelemetry);
1717+
17001718
//load OCPP library modules: Mongoose WS adapter and Core OCPP library
17011719

17021720
auto filesystem = MicroOcpp::makeDefaultFilesystemAdapter(
@@ -1814,6 +1832,9 @@ void ocppInit() {
18141832
return nullptr;
18151833
});
18161834

1835+
// Track LoadBl state at init time for runtime exclusivity checks
1836+
OcppWasStandalone = !LoadBl;
1837+
18171838
// If SmartEVSE load balancer is turned off, then enable OCPP Smart Charging
18181839
// This means after toggling LB, OCPP must be disabled and enabled for changes to become effective
18191840
if (!LoadBl) {
@@ -1866,6 +1887,21 @@ void ocppInit() {
18661887
OcppDefinedTxNotification = true;
18671888
OcppTrackTxNotification = event;
18681889
OcppLastTxNotification = millis();
1890+
1891+
// Update telemetry counters
1892+
if (event == MicroOcpp::TxNotification::StartTx) {
1893+
ocpp_telemetry_tx_started(&OcppTelemetry);
1894+
} else if (event == MicroOcpp::TxNotification::StopTx) {
1895+
ocpp_telemetry_tx_stopped(&OcppTelemetry);
1896+
} else if (event == MicroOcpp::TxNotification::Authorized ||
1897+
event == MicroOcpp::TxNotification::RemoteStart) {
1898+
ocpp_telemetry_auth_accepted(&OcppTelemetry);
1899+
} else if (event == MicroOcpp::TxNotification::AuthorizationRejected ||
1900+
event == MicroOcpp::TxNotification::DeAuthorized) {
1901+
ocpp_telemetry_auth_rejected(&OcppTelemetry);
1902+
} else if (event == MicroOcpp::TxNotification::AuthorizationTimeout) {
1903+
ocpp_telemetry_auth_timeout(&OcppTelemetry);
1904+
}
18691905
});
18701906

18711907
// Declare custom "ConfigureMaxCurrent" key
@@ -1908,6 +1944,7 @@ void ocppDeinit() {
19081944
OcppTrackAccessBit = false;
19091945
OcppTrackCPvoltage = PILOT_NOK;
19101946
OcppCurrentLimit = -1.f;
1947+
OcppWasStandalone = false;
19111948

19121949
mocpp_deinitialize();
19131950

@@ -1923,6 +1960,26 @@ void ocppLoop() {
19231960

19241961
mocpp_loop();
19251962

1963+
// Check OCPP / LoadBl mutual exclusivity at runtime
1964+
ocpp_lb_status_t lb_status = ocpp_check_lb_exclusivity(LoadBl, OcppMode, OcppWasStandalone);
1965+
OcppTelemetry.lb_conflict = (lb_status != OCPP_LB_OK);
1966+
if (lb_status == OCPP_LB_CONFLICT) {
1967+
// LoadBl changed to non-zero while OCPP is active — Smart Charging limits
1968+
// are silently ignored by the state machine. Neutralize the limit and warn.
1969+
if (OcppCurrentLimit >= 0.0f) {
1970+
_LOG_W("OCPP: LoadBl=%u conflicts with Smart Charging, disabling OCPP current limit\n", LoadBl);
1971+
OcppCurrentLimit = -1.0f;
1972+
}
1973+
} else if (lb_status == OCPP_LB_NEEDS_REINIT) {
1974+
// LoadBl changed from non-zero to 0 — Smart Charging callback was never
1975+
// registered. User needs to disable/enable OCPP for it to take effect.
1976+
static bool ocpp_reinit_warned = false;
1977+
if (!ocpp_reinit_warned) {
1978+
_LOG_W("OCPP: LoadBl changed to standalone but Smart Charging not registered. Disable/enable OCPP to activate.\n");
1979+
ocpp_reinit_warned = true;
1980+
}
1981+
}
1982+
19261983
// handle Configuration updates
19271984

19281985
auto config = MicroOcpp::getConfigurationPublic("ConfigureMaxCurrent");
@@ -1973,13 +2030,22 @@ void ocppLoop() {
19732030
if (RFIDReader == 6 || RFIDReader == 0) {
19742031
// RFID reader in OCPP mode or RFID fully disabled - OCPP controls Access_bit
19752032
if (!OcppTrackPermitsCharge && ocppPermitsCharge()) {
1976-
_LOG_A("OCPP set Access_bit\n");
1977-
setAccess(ON);
2033+
// Guard: defer Access_bit if mode/delay conflicts (FreeVend + Solar, ChargeDelay)
2034+
if (ocpp_should_defer_access(Mode, ChargeDelay, ErrorFlags)) {
2035+
// Don't update OcppTrackPermitsCharge — retry rising edge on next loop
2036+
_LOG_D("OCPP: deferring Access_bit (Mode=%u ChargeDelay=%u ErrorFlags=0x%04X)\n", Mode, ChargeDelay, ErrorFlags);
2037+
} else {
2038+
_LOG_A("OCPP set Access_bit\n");
2039+
setAccess(ON);
2040+
OcppTrackPermitsCharge = true;
2041+
}
19782042
} else if (AccessStatus == ON && !ocppPermitsCharge()) {
19792043
_LOG_A("OCPP unset Access_bit\n");
19802044
setAccess(OFF);
2045+
OcppTrackPermitsCharge = false;
2046+
} else {
2047+
OcppTrackPermitsCharge = ocppPermitsCharge();
19812048
}
1982-
OcppTrackPermitsCharge = ocppPermitsCharge();
19832049

19842050
// Check if OCPP charge permission has been revoked by other module
19852051
if (OcppTrackPermitsCharge && // OCPP has set Acess_bit and still allows charge

SmartEVSE-3/src/http_handlers.cpp

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include <MicroOcpp.h>
2121
#include <MicroOcppMongooseClient.h>
2222
#include <MicroOcpp/Core/Configuration.h>
23+
#include "ocpp_logic.h"
24+
#include "ocpp_telemetry.h"
2325
#endif //ENABLE_OCPP
2426

2527
// Externs for globals not exposed via headers
@@ -81,6 +83,8 @@ extern uint16_t MQTTHeartbeat;
8183

8284
#if ENABLE_OCPP && defined(SMARTEVSE_VERSION)
8385
extern MicroOcpp::MOcppMongooseClient *OcppWsClient;
86+
extern float OcppCurrentLimit;
87+
extern ocpp_telemetry_t OcppTelemetry;
8488
#endif
8589

8690
//make mongoose 7.14 compatible with 7.13
@@ -259,6 +263,17 @@ bool handle_URI(struct mg_connection *c, struct mg_http_message *hm, webServerR
259263
} else {
260264
doc["ocpp"]["status"] = "Disconnected";
261265
}
266+
267+
// OCPP telemetry
268+
doc["ocpp"]["tx_active"] = OcppTelemetry.tx_active;
269+
doc["ocpp"]["tx_starts"] = OcppTelemetry.tx_start_count;
270+
doc["ocpp"]["tx_stops"] = OcppTelemetry.tx_stop_count;
271+
doc["ocpp"]["auth_accepts"] = OcppTelemetry.auth_accept_count;
272+
doc["ocpp"]["auth_rejects"] = OcppTelemetry.auth_reject_count;
273+
doc["ocpp"]["auth_timeouts"] = OcppTelemetry.auth_timeout_count;
274+
doc["ocpp"]["smart_charging_active"] = (!LoadBl && OcppCurrentLimit >= 0.0f);
275+
doc["ocpp"]["current_limit_a"] = OcppCurrentLimit >= 0.0f ? OcppCurrentLimit : -1;
276+
doc["ocpp"]["lb_conflict"] = OcppTelemetry.lb_conflict;
262277
#endif //ENABLE_OCPP
263278

264279
doc["home_battery"]["current"] = homeBatteryCurrent;
@@ -633,26 +648,44 @@ bool handle_URI(struct mg_connection *c, struct mg_http_message *hm, webServerR
633648
}
634649

635650
if(request->hasParam("ocpp_backend_url")) {
636-
if (OcppWsClient) {
637-
OcppWsClient->setBackendUrl(request->getParam("ocpp_backend_url")->value().c_str());
651+
const char *url = request->getParam("ocpp_backend_url")->value().c_str();
652+
ocpp_validate_result_t vr = ocpp_validate_backend_url(url);
653+
if (vr != OCPP_VALIDATE_OK) {
654+
doc["ocpp_backend_url"] = vr == OCPP_VALIDATE_EMPTY ? "URL is empty"
655+
: vr == OCPP_VALIDATE_BAD_SCHEME ? "URL must start with ws:// or wss://"
656+
: "Invalid URL";
657+
} else if (OcppWsClient) {
658+
OcppWsClient->setBackendUrl(url);
638659
doc["ocpp_backend_url"] = OcppWsClient->getBackendUrl();
639660
} else {
640661
doc["ocpp_backend_url"] = "Can only update when OCPP enabled";
641662
}
642663
}
643664

644665
if(request->hasParam("ocpp_cb_id")) {
645-
if (OcppWsClient) {
646-
OcppWsClient->setChargeBoxId(request->getParam("ocpp_cb_id")->value().c_str());
666+
const char *cb_id = request->getParam("ocpp_cb_id")->value().c_str();
667+
ocpp_validate_result_t vr = ocpp_validate_chargebox_id(cb_id);
668+
if (vr != OCPP_VALIDATE_OK) {
669+
doc["ocpp_cb_id"] = vr == OCPP_VALIDATE_EMPTY ? "ChargeBoxId is empty"
670+
: vr == OCPP_VALIDATE_TOO_LONG ? "ChargeBoxId exceeds 20 characters"
671+
: vr == OCPP_VALIDATE_BAD_CHARS ? "ChargeBoxId contains invalid characters"
672+
: "Invalid ChargeBoxId";
673+
} else if (OcppWsClient) {
674+
OcppWsClient->setChargeBoxId(cb_id);
647675
doc["ocpp_cb_id"] = OcppWsClient->getChargeBoxId();
648676
} else {
649677
doc["ocpp_cb_id"] = "Can only update when OCPP enabled";
650678
}
651679
}
652680

653681
if(request->hasParam("ocpp_auth_key")) {
654-
if (OcppWsClient) {
655-
OcppWsClient->setAuthKey(request->getParam("ocpp_auth_key")->value().c_str());
682+
const char *auth_key = request->getParam("ocpp_auth_key")->value().c_str();
683+
ocpp_validate_result_t vr = ocpp_validate_auth_key(auth_key);
684+
if (vr != OCPP_VALIDATE_OK) {
685+
doc["ocpp_auth_key"] = vr == OCPP_VALIDATE_TOO_LONG ? "Auth key exceeds 40 characters"
686+
: "Invalid auth key";
687+
} else if (OcppWsClient) {
688+
OcppWsClient->setAuthKey(auth_key);
656689
doc["ocpp_auth_key"] = OcppWsClient->getAuthKey();
657690
} else {
658691
doc["ocpp_auth_key"] = "Can only update when OCPP enabled";

SmartEVSE-3/src/main.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
#include <MicroOcppMongooseClient.h>
5656
#include <MicroOcpp/Core/Configuration.h>
5757
#include <MicroOcpp/Core/Context.h>
58+
#include "ocpp_telemetry.h"
5859
#endif //ENABLE_OCPP
5960

6061
extern Preferences preferences;
@@ -293,6 +294,8 @@ uint8_t OcppTrackCPvoltage = PILOT_NOK; //track positive part of CP signal for O
293294
MicroOcpp::MOcppMongooseClient *OcppWsClient;
294295

295296
float OcppCurrentLimit = -1.f; // Negative value: no OCPP limit defined
297+
bool OcppWasStandalone = false; // Tracks LoadBl state at ocppInit() time for LB exclusivity check
298+
ocpp_telemetry_t OcppTelemetry; // OCPP connection and transaction telemetry
296299

297300
unsigned long OcppStopReadingSyncTime; // Stop value synchronization: delay StopTransaction by a few seconds so it reports an accurate energy reading
298301

SmartEVSE-3/src/mqtt_publish.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ typedef enum {
7676
MQTT_SLOT_SCHEDULE_STATE,
7777
MQTT_SLOT_FREE_HEAP,
7878
MQTT_SLOT_MQTT_MSG_COUNT,
79+
MQTT_SLOT_OCPP_TX_ACTIVE,
80+
MQTT_SLOT_OCPP_CURRENT_LIMIT,
81+
MQTT_SLOT_OCPP_SMART_CHARGING,
7982
MQTT_SLOT_COUNT /* must be <= MQTT_CACHE_MAX_SLOTS */
8083
} mqtt_slot_t;
8184

SmartEVSE-3/src/ocpp_logic.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,28 @@ bool ocpp_should_clear_access(bool permits_charge, uint8_t access_status) {
5454
return access_status == 1 && !permits_charge; /* 1 = ON */
5555
}
5656

57+
bool ocpp_should_defer_access(uint8_t mode, uint8_t charge_delay,
58+
uint16_t error_flags) {
59+
/*
60+
* When FreeVend/auto-auth is active, OCPP grants ocppPermitsCharge()
61+
* unconditionally. But in Solar mode, we should defer Access_bit until
62+
* the state machine's solar logic confirms surplus is available.
63+
*
64+
* Deferral conditions:
65+
* 1. Solar mode + NO_SUN error → no solar surplus available
66+
* 2. Any mode + ChargeDelay active → still in delay period
67+
*/
68+
if (mode == MODE_SOLAR && (error_flags & NO_SUN)) {
69+
return true;
70+
}
71+
72+
if (charge_delay > 0) {
73+
return true;
74+
}
75+
76+
return false;
77+
}
78+
5779
/* ---- RFID hex formatting ---- */
5880

5981
void ocpp_format_rfid_hex(const uint8_t *rfid, size_t rfid_len,

SmartEVSE-3/src/ocpp_logic.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ bool ocpp_should_set_access(bool permits_charge, bool prev_permits_charge);
5252
*/
5353
bool ocpp_should_clear_access(bool permits_charge, uint8_t access_status);
5454

55+
/*
56+
* Returns true if setting Access_bit should be deferred despite OCPP
57+
* permitting charge. This guards against FreeVend/auto-auth bypassing
58+
* Solar mode surplus checks or ChargeDelay.
59+
*
60+
* mode — current Mode (MODE_NORMAL=0, MODE_SMART=1, MODE_SOLAR=2)
61+
* charge_delay — current ChargeDelay (>0 means delay active)
62+
* error_flags — current ErrorFlags (NO_SUN bit means no solar surplus)
63+
*/
64+
bool ocpp_should_defer_access(uint8_t mode, uint8_t charge_delay,
65+
uint16_t error_flags);
66+
5567
/* ---- RFID hex formatting ---- */
5668

5769
#define OCPP_RFID_HEX_MAX 15 /* 7 bytes * 2 hex chars + NUL */

SmartEVSE-3/src/ocpp_telemetry.c

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* ocpp_telemetry.c - OCPP connection and transaction telemetry
3+
*
4+
* Pure C counter management. All functions are simple and safe to call
5+
* from any context. The struct is ~40 bytes — well within RAM budget.
6+
*/
7+
8+
#include "ocpp_telemetry.h"
9+
#include <string.h>
10+
11+
void ocpp_telemetry_init(ocpp_telemetry_t *t) {
12+
if (!t) return;
13+
memset(t, 0, sizeof(*t));
14+
}
15+
16+
void ocpp_telemetry_ws_connected(ocpp_telemetry_t *t) {
17+
if (!t) return;
18+
t->ws_connect_count++;
19+
}
20+
21+
void ocpp_telemetry_ws_disconnected(ocpp_telemetry_t *t) {
22+
if (!t) return;
23+
t->ws_disconnect_count++;
24+
}
25+
26+
void ocpp_telemetry_tx_started(ocpp_telemetry_t *t) {
27+
if (!t) return;
28+
t->tx_start_count++;
29+
t->tx_active = true;
30+
}
31+
32+
void ocpp_telemetry_tx_stopped(ocpp_telemetry_t *t) {
33+
if (!t) return;
34+
t->tx_stop_count++;
35+
t->tx_active = false;
36+
}
37+
38+
void ocpp_telemetry_auth_accepted(ocpp_telemetry_t *t) {
39+
if (!t) return;
40+
t->auth_accept_count++;
41+
}
42+
43+
void ocpp_telemetry_auth_rejected(ocpp_telemetry_t *t) {
44+
if (!t) return;
45+
t->auth_reject_count++;
46+
}
47+
48+
void ocpp_telemetry_auth_timeout(ocpp_telemetry_t *t) {
49+
if (!t) return;
50+
t->auth_timeout_count++;
51+
}

0 commit comments

Comments
 (0)