Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions SmartEVSE-3/src/ocpp_logic.c
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,43 @@ ocpp_validate_result_t ocpp_validate_auth_key(const char *auth_key) {

return OCPP_VALIDATE_OK;
}

/* ---- IEC 61851 → OCPP StatusNotification mapping ---- */

const char *ocpp_iec61851_to_status(char iec_state, bool evse_ready,
bool tx_active) {
switch (iec_state) {
case 'A':
/* No vehicle connected. If a transaction just ended, MicroOcpp may
* report Finishing briefly — but from an IEC 61851 perspective, this
* is Available. */
return tx_active ? OCPP_STATUS_FINISHING : OCPP_STATUS_AVAILABLE;

case 'B':
/* Vehicle connected but not charging. During a transaction, the EV
* has paused charging (SuspendedEV). Otherwise, it's Preparing. */
return tx_active ? OCPP_STATUS_SUSPENDED_EV : OCPP_STATUS_PREPARING;

case 'C':
/* Vehicle charging. If EVSE is not offering current (e.g., OCPP
* limit set to 0 or load balancer paused), it's SuspendedEVSE. */
if (!evse_ready) {
return OCPP_STATUS_SUSPENDED_EVSE;
}
return OCPP_STATUS_CHARGING;

case 'D':
/* Charging with ventilation — same as C for OCPP purposes. */
if (!evse_ready) {
return OCPP_STATUS_SUSPENDED_EVSE;
}
return OCPP_STATUS_CHARGING;

case 'E':
case 'F':
return OCPP_STATUS_FAULTED;

default:
return OCPP_STATUS_FAULTED;
}
}
33 changes: 33 additions & 0 deletions SmartEVSE-3/src/ocpp_logic.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,39 @@ ocpp_validate_result_t ocpp_validate_chargebox_id(const char *cb_id);
*/
ocpp_validate_result_t ocpp_validate_auth_key(const char *auth_key);

/* ---- IEC 61851 → OCPP StatusNotification mapping ---- */

/*
* OCPP 1.6 ChargePointStatus values as string constants.
* These match the StatusNotification.req status field.
*/
#define OCPP_STATUS_AVAILABLE "Available"
#define OCPP_STATUS_PREPARING "Preparing"
#define OCPP_STATUS_CHARGING "Charging"
#define OCPP_STATUS_SUSPENDED_EVSE "SuspendedEVSE"
#define OCPP_STATUS_SUSPENDED_EV "SuspendedEV"
#define OCPP_STATUS_FINISHING "Finishing"
#define OCPP_STATUS_FAULTED "Faulted"

/*
* Map IEC 61851 state letter to OCPP 1.6 ChargePointStatus string.
* iec_state — IEC 61851 state ('A'-'F' from evse_state_to_iec61851())
* evse_ready — true if EVSE is offering current (PWM > 0)
* tx_active — true if an OCPP transaction is running
*
* Returns a pointer to a static string constant (never NULL).
*
* Mapping:
* A (no vehicle) → Available (or Finishing if tx just ended)
* B (vehicle connected) → Preparing (or SuspendedEV if tx active but EV not drawing)
* C (charging) → Charging (or SuspendedEVSE if EVSE not offering current)
* D (with ventilation) → Charging
* E (error) → Faulted
* F (not available) → Faulted
*/
const char *ocpp_iec61851_to_status(char iec_state, bool evse_ready,
bool tx_active);

#ifdef __cplusplus
}
#endif
Expand Down
3 changes: 3 additions & 0 deletions SmartEVSE-3/test/native/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ $(BUILD)/test_ocpp_settings: $(TEST_DIR)/test_ocpp_settings.c $(OCPP_LOGIC_SRC)
$(BUILD)/test_ocpp_telemetry: $(TEST_DIR)/test_ocpp_telemetry.c $(OCPP_TELEMETRY_SRC) $(SRC_DIR)/ocpp_telemetry.h include/*.h | $(BUILD)
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_ocpp_telemetry.c $(OCPP_TELEMETRY_SRC)

$(BUILD)/test_ocpp_iec61851: $(TEST_DIR)/test_ocpp_iec61851.c $(OCPP_LOGIC_SRC) $(SRC_DIR)/ocpp_logic.h include/*.h | $(BUILD)
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_ocpp_iec61851.c $(OCPP_LOGIC_SRC)

# Build each state machine test binary (generic rule)
$(BUILD)/test_%: $(TEST_DIR)/test_%.c $(EVSE_SRC) include/*.h $(SRC_DIR)/*.h | $(BUILD)
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(TEST_DIR)/test_$*.c $(EVSE_SRC)
Expand Down
183 changes: 183 additions & 0 deletions SmartEVSE-3/test/native/tests/test_ocpp_iec61851.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* test_ocpp_iec61851.c - IEC 61851 to OCPP StatusNotification mapping tests
*
* Tests the pure C mapping from IEC 61851 state letters (A-F) to OCPP 1.6
* ChargePointStatus values. This mapping is used by EVCC and other external
* controllers that need accurate CP state reporting via OCPP.
*/

#include "test_framework.h"
#include "ocpp_logic.h"
#include <string.h>

/* ---- State A: No vehicle connected ---- */

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-090
* @scenario State A without active transaction maps to Available
* @given IEC 61851 state is A (no vehicle), no transaction active
* @when ocpp_iec61851_to_status is called
* @then Returns "Available"
*/
void test_iec_a_no_tx_available(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_AVAILABLE,
ocpp_iec61851_to_status('A', false, false));
}

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-090
* @scenario State A with active transaction maps to Finishing
* @given IEC 61851 state is A (no vehicle), transaction still active (just unplugged)
* @when ocpp_iec61851_to_status is called
* @then Returns "Finishing" because the transaction is ending
*/
void test_iec_a_tx_active_finishing(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_FINISHING,
ocpp_iec61851_to_status('A', false, true));
}

/* ---- State B: Vehicle connected, not charging ---- */

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-091
* @scenario State B without transaction maps to Preparing
* @given IEC 61851 state is B (vehicle connected), no transaction
* @when ocpp_iec61851_to_status is called
* @then Returns "Preparing" because the vehicle is waiting for authorization
*/
void test_iec_b_no_tx_preparing(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_PREPARING,
ocpp_iec61851_to_status('B', true, false));
}

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-091
* @scenario State B with active transaction maps to SuspendedEV
* @given IEC 61851 state is B (connected but not drawing), transaction active
* @when ocpp_iec61851_to_status is called
* @then Returns "SuspendedEV" because the EV has paused charging
*/
void test_iec_b_tx_active_suspended_ev(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_SUSPENDED_EV,
ocpp_iec61851_to_status('B', true, true));
}

/* ---- State C: Charging ---- */

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-092
* @scenario State C with EVSE offering current maps to Charging
* @given IEC 61851 state is C (charging), EVSE ready (PWM > 0)
* @when ocpp_iec61851_to_status is called
* @then Returns "Charging"
*/
void test_iec_c_evse_ready_charging(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_CHARGING,
ocpp_iec61851_to_status('C', true, true));
}

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-092
* @scenario State C with EVSE not offering current maps to SuspendedEVSE
* @given IEC 61851 state is C, EVSE not ready (current = 0, e.g. OCPP limit)
* @when ocpp_iec61851_to_status is called
* @then Returns "SuspendedEVSE" because the EVSE has paused charging
*/
void test_iec_c_evse_not_ready_suspended_evse(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_SUSPENDED_EVSE,
ocpp_iec61851_to_status('C', false, true));
}

/* ---- State D: Charging with ventilation ---- */

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-093
* @scenario State D with EVSE ready maps to Charging
* @given IEC 61851 state is D (charging with ventilation), EVSE ready
* @when ocpp_iec61851_to_status is called
* @then Returns "Charging" (same as State C for OCPP)
*/
void test_iec_d_evse_ready_charging(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_CHARGING,
ocpp_iec61851_to_status('D', true, true));
}

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-093
* @scenario State D with EVSE not ready maps to SuspendedEVSE
* @given IEC 61851 state is D, EVSE not ready
* @when ocpp_iec61851_to_status is called
* @then Returns "SuspendedEVSE"
*/
void test_iec_d_evse_not_ready_suspended_evse(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_SUSPENDED_EVSE,
ocpp_iec61851_to_status('D', false, true));
}

/* ---- State E/F: Error / Not available ---- */

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-094
* @scenario State E maps to Faulted
* @given IEC 61851 state is E (error)
* @when ocpp_iec61851_to_status is called
* @then Returns "Faulted"
*/
void test_iec_e_faulted(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_FAULTED,
ocpp_iec61851_to_status('E', false, false));
}

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-094
* @scenario State F maps to Faulted
* @given IEC 61851 state is F (not available)
* @when ocpp_iec61851_to_status is called
* @then Returns "Faulted"
*/
void test_iec_f_faulted(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_FAULTED,
ocpp_iec61851_to_status('F', false, false));
}

/*
* @feature OCPP IEC 61851 Status Mapping
* @req REQ-OCPP-094
* @scenario Unknown state maps to Faulted
* @given IEC 61851 state is an invalid character
* @when ocpp_iec61851_to_status is called
* @then Returns "Faulted" as a safe default
*/
void test_iec_unknown_faulted(void) {
TEST_ASSERT_EQUAL_STRING(OCPP_STATUS_FAULTED,
ocpp_iec61851_to_status('X', false, false));
}

/* ---- Main ---- */
int main(void) {
TEST_SUITE_BEGIN("OCPP IEC 61851 Status Mapping");

RUN_TEST(test_iec_a_no_tx_available);
RUN_TEST(test_iec_a_tx_active_finishing);
RUN_TEST(test_iec_b_no_tx_preparing);
RUN_TEST(test_iec_b_tx_active_suspended_ev);
RUN_TEST(test_iec_c_evse_ready_charging);
RUN_TEST(test_iec_c_evse_not_ready_suspended_evse);
RUN_TEST(test_iec_d_evse_ready_charging);
RUN_TEST(test_iec_d_evse_not_ready_suspended_evse);
RUN_TEST(test_iec_e_faulted);
RUN_TEST(test_iec_f_faulted);
RUN_TEST(test_iec_unknown_faulted);

TEST_SUITE_RESULTS();
}
82 changes: 82 additions & 0 deletions docs/ocpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,85 @@ As final step you need to select the chargepoint in the "grid rewards" part of t
## SteVe
[@jpiscaer](https://github.com/jpiscaer) is using [SteVe](https://github.com/steve-community/steve), a free and open source OCPP back-end, available to self-host (running the default docker compose config in his case). The use case for using SteVe is to keep a list of charging transactions for reimbursement of charging costs from his company.
With a monthly list of transactions and a read-out of my (Inepro PRO380-Mod) kWh meter, he can satisfy the Belastingdienst's requirements. Note that he's not automating the payouts; he does the financial transactions manually, so he doesn't need a payment provider (nor does SteVe support this). If you need automatic payouts, look for Tap Electric or Monta. Since the EVSE is on a private driveway, he doesn't want or need to use RFID tags; he's has set the OCPP configuration to 'auto-authorize', and has configured SteVe to accept all requests through a dummy rfid tag. This way, he only uses SteVe to keep a list of charging transactions, and he doesn't allow (keep track of) guest usage, public usage etc.

---

## OCPP Configuration Reference

### Settings

| Parameter | Description | Validation |
|-----------|-------------|------------|
| Backend URL | WebSocket endpoint for the OCPP backend | Must start with `ws://` or `wss://` |
| Charge Box ID | Identifier for this charge point | Max 20 characters, printable ASCII |
| Password | WebSocket Basic Auth key | Max 40 characters (optional) |
| Auto Authorize | Enable FreeVend (auto-start transactions) | On/Off |
| Auto Authorize ID Tag | ID tag used for auto-authorized transactions | String |

### OCPP and Load Balancing

OCPP Smart Charging and SmartEVSE internal load balancing are **mutually exclusive**:

- **LoadBl = 0 (Standalone):** OCPP Smart Charging is active. The backend can set current limits via `SetChargingProfile`.
- **LoadBl = 1+ (Master/Node):** OCPP Smart Charging is disabled. The internal load balancer controls current distribution. OCPP still tracks transactions and authorization but cannot control current.

If you change LoadBl while OCPP is active, the firmware detects the conflict and disables OCPP current limiting. You must disable and re-enable OCPP for the new setting to take full effect.

### OCPP and Solar/Smart Mode

When Auto Authorize (FreeVend) is enabled together with Solar mode, the firmware defers granting charge permission until solar surplus is available. This prevents FreeVend from bypassing the solar surplus check. The same applies when a ChargeDelay is active.

### OCPP Telemetry

The `/settings` API endpoint includes OCPP diagnostics under the `ocpp` key:

| Field | Description |
|-------|-------------|
| `tx_active` | Whether a transaction is currently running |
| `tx_starts` / `tx_stops` | Transaction start/stop counters since boot |
| `auth_accepts` / `auth_rejects` / `auth_timeouts` | Authorization result counters |
| `smart_charging_active` | Whether OCPP Smart Charging is controlling current |
| `current_limit_a` | Current OCPP limit in amps (-1 = no limit) |
| `lb_conflict` | Whether a LoadBl/OCPP conflict is detected |

MQTT topics published:
- `<prefix>/OCPPTxActive` — `true` / `false`
- `<prefix>/OCPPCurrentLimit` — float or `none`
- `<prefix>/OCPPSmartCharging` — `Active` / `Inactive` / `Conflict`

---

## FAQ / Troubleshooting

### "OCPP shows Disconnected but charging still works"

This is normal. MicroOcpp has an offline transaction queue. When the WebSocket connection drops, transactions are cached locally and sent to the backend when the connection is restored. Charging continues using the last known authorization state.

### "Car charges immediately without RFID"

FreeVend (Auto Authorize) is enabled. When active, MicroOcpp automatically starts a transaction when a vehicle is plugged in, without requiring an RFID card. To require RFID authentication, disable Auto Authorize in the OCPP settings.

### "OCPP current limit is not working"

Check these in order:
1. **LoadBl must be 0 (Standalone).** OCPP Smart Charging only works when the internal load balancer is disabled. Check the `/settings` API — `lb_conflict` should be `false`.
2. **Backend must send SetChargingProfile.** The limit is set by the backend, not configured locally. Check `current_limit_a` in `/settings` — if it shows `-1`, no limit has been received.
3. **MinCurrent applies.** If the OCPP limit is below MinCurrent (typically 6A), the charge current is zeroed rather than set to the limit value.

### "Dual chargers report wrong energy"

SmartEVSE initializes MicroOcpp with a single connector. Each ESP32 runs its own OCPP instance. If both connect to the same backend, ensure each has a unique Charge Box ID. There is no multi-connector OCPP coordination between two SmartEVSEs — each reports independently.

### "Connection drops every few hours"

Common causes:
- **WiFi stability:** Check WiFi signal strength (RSSI) via MQTT or `/settings`. Consider a WiFi repeater if RSSI is below -70 dBm.
- **Backend timeouts:** Some backends disconnect idle WebSocket connections. MicroOcpp has internal reconnect logic and will reconnect automatically.
- **Router settings:** Some routers aggressively close idle TCP connections. Check your router's TCP timeout settings.
- If the problem persists, check the OCPP telemetry counters in `/settings` — the `ws_connects` and `ws_disconnects` fields show reconnection frequency since boot.

### "Settings validation rejects my URL/ChargeBoxId"

- **URL:** Must start with `ws://` or `wss://` and include a host after the scheme. Example: `wss://ocpp.provider.com/charger123`
- **Charge Box ID:** Max 20 characters (OCPP 1.6 CiString20 limit). Only printable ASCII characters are allowed. Special characters like `<`, `>`, `&`, `"`, `'` are rejected.
- **Password:** Max 40 characters (OCPP 1.6 AuthorizationKey limit). Empty password is valid (no auth).
Loading