feat(ocpp-cp): chain the v201 ISO 15118 smart-charging negotiation (M7) - #578
Merged
Merged
Conversation
Add `ChargePoint::negotiate_ev_charging`, a driver hook that chains the two existing CP-initiated legs into the real ISO 15118 smart-charging sequence a station performs when an EV plugs in: report the EV's declared needs (`NotifyEVChargingNeeds`), then — gated on the CSMS's needs status — report the schedule the EV intends to follow (`NotifyEVChargingSchedule`). The needs status gates the schedule leg: `Accepted` runs it and surfaces its process-only `GenericStatus`; `Rejected` / `Processing` stop the sequence without emitting the schedule. A new typed `EvChargingNegotiationOutcome` records which legs ran and the terminal status so the sequence is assertable. Composes the two hooks with no new wire message and reuses their guards: the needs leg enforces V201-only and `evseId > 0` before anything reaches the wire, so an unsupported version → `NotSupported` and a non-positive evse_id → `ValidationError`, with nothing emitted. Each status arm is handled explicitly (no panic); transport/CALLERROR failures on either leg propagate as `OcppError`. The EV's needs and schedule are injected deterministically by the caller (a pure simulator has no real EV), matching the other v201-only behavior-injection hooks. mobilityhouse/ocpp is protocol-only (it models each message independently, no chained driver), so the message semantics are ported from ocpp/v201/call.py (`NotifyEVChargingNeeds`, `NotifyEVChargingSchedule`) and the sequencing is written idiomatically as the simulator's own responsibility. Tests (via the capturing mock CSMS): Accepted needs runs both legs needs-first; Accepted needs with a Rejected schedule surfaces that status; Rejected and Processing needs skip the schedule leg (asserted off the wire); non-positive evse_id and a 1.6J station are both refused. Closes #569 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Chqxsy7wrYYWr9ducD5xj5
duyhuynh-vn
pushed a commit
that referenced
this pull request
Sep 27, 2026
Resolves the lib.rs conflict from #578 (EvChargingNegotiationOutcome) landing alongside this PR's ChargingStateChangeOutcome enum. Both outcome enums are independent additions at the same location; kept both. fmt/clippy/test --workspace all green after resolution. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9MRGtFszCXLVdJKQrWUND
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
ChargePoint::negotiate_ev_charging, a thin driver that chains the two CP-initiated smart-charging legs into the real ISO 15118 negotiation a station performs when an EV plugs in: report the EV's declared needs, then — gated on the CSMS's response — report the schedule the EV intends to follow. Advances M7 — OCPP 2.0.1.Closes #569Real use case
Today a caller must orchestrate
request_notify_ev_charging_needsandrequest_notify_ev_charging_scheduleby hand and re-implement the gating rule every time. On a real station the sequence is fixed: the station reports the EV's needs, and only if the CSMS accepts them does it push the schedule the EV intends to follow (onRejectedthe smart-charging service is unavailable; onProcessingthe CSMS is still computing and the station waits for aSetChargingProfilerather than pushing a schedule). This hook makes the simulator drive that exact sequence, so a CSMS integration test can exercise the whole plug-in negotiation with one call and assert the terminal state deterministically.What changed
crates/ocpp-cp/src/lib.rsonly.EvChargingNegotiationOutcome— a small typed outcome enum capturing which legs ran and the terminal status:NeedsRejected,NeedsProcessing(needs leg only; schedule not emitted), andAccepted { schedule_status }(both legs ran; carries the schedule leg's process-onlyGenericStatusEnumType). Mirrors the existingMonitorTripOutcomeidiom.negotiate_ev_charging(evse_id, charging_needs, max_schedule_tuples, time_base, charging_schedule)— runs the needs leg, then matches its status:Accepted→ run the schedule leg and returnAccepted { schedule_status };Rejected/Processing→ return the corresponding variant without emitting the schedule.Design / trust boundary:
evseId > 0before any CALL is emitted, so an unsupported version surfaces asOcppError::NotSupportedand a non-positiveevse_idasOcppError::ValidationError, with nothing on the wire (no partial negotiation).Rejected/Processingneeds status is a valid protocol outcome returned asOk(..), not anErr.OcppErrorvia?.unlock_outcome, firmware fault injection, andtrip_variable_monitor. Injected values ride to the wire verbatim through each leg'scall()schema validation.What was ported
mobilityhouse/ocpp is protocol-only and models each message independently (no chained driver), so the message semantics are ported and the sequencing is written idiomatically as the simulator's own responsibility.
ocpp/v201/call.py—NotifyEVChargingNeeds,NotifyEVChargingSchedule(the two legs already ported).ocpp/v201/enums.py—NotifyEVChargingNeedsStatusEnumType,GenericStatusEnumType.Test plan
cargo fmt --all --check✅cargo clippy --all-targets -- -D warnings✅cargo test --workspace✅ (all green)New tests (via the capturing mock CSMS, so each asserts exactly what reached the wire and in what order):
Acceptedneeds → both legs run needs-first, outcomeAccepted { Accepted }.Acceptedneeds +Rejectedschedule → outcomeAccepted { Rejected }, both legs ran.Rejectedneeds → outcomeNeedsRejected, schedule leg not on the wire.Processingneeds → outcomeNeedsProcessing, schedule leg not on the wire.evse_id→ValidationError(needs guard fires first, nothing emitted).NotSupported.Acceptance criteria
Accepted→ schedule emitted;Rejected→ not;Processing→ surfaced, not emitted).evseId > 0.cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.Known gaps / notes
SetChargingProfilelimits (the CSMS side) is out of scope — this is CP-origination sequencing only. Direction tracked in Direction: v201 CALL/CALLRESULT message coverage is complete — what's the next M7→M8 track? #256.🤖 Generated with Claude Code
https://claude.ai/code/session_01Chqxsy7wrYYWr9ducD5xj5
Generated by Claude Code