You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The v201 transaction lifecycle in crates/ocpp-cp/src/v201_transaction.rs currently reports only two charging states across an entire session: Charging on Started/Updated and Idle on Ended. transaction_event_updated() hardcodes chargingState = Charging and triggerReason = MeterValuePeriodic (verified — v201_transaction.rs:258–289). A simulator-wide inventory confirms the gap: ChargingStateEnumType::{SuspendedEV, SuspendedEVSE, EVConnected} and TriggerReasonEnumType::ChargingStateChanged are never constructed anywhere in crates/ocpp-cp.
Real charging stations emit an interim TransactionEvent(Updated) with triggerReason = ChargingStateChanged whenever the session's charging state transitions — the EV pauses draw (SuspendedEV), the EVSE/CSMS pauses delivery (e.g. a 0 A charging profile / load management → SuspendedEVSE), or charging resumes (Charging). Without these, a downstream CSMS/CDR sees a session that is "always charging" until it abruptly ends — not faithful to OCPP 2.0.1 Part 2, and it hides the suspend semantics that smart-charging conformance exercises.
Scope (small slice)
Mid-transaction suspend/resume only: transitions among Charging ↔ SuspendedEV ↔ SuspendedEVSE, each emitted as one TransactionEvent(Updated) with triggerReason = ChargingStateChanged and the new transactionInfo.chargingState.
A pure builder alongside the existing ones (e.g. transaction_event_charging_state_changed(session, seq_no, new_state, timestamp)), plus a V201-only driver hook on the simulator to drive a connector into SuspendedEV/SuspendedEVSE and back to Charging — the same opt-in behavior-injection pattern as the monitor-trip (port(m7): CP simulator 2.0.1 — emit NotifyEvent when a CSMS-installed variable monitor trips #545) / charging-limit hooks.
seqNo continues the transaction's monotonic stream (strictly between Started and Ended); no idToken (already authorized); a pure state-change event carries chargingState, not a Sample.Periodic reading — leave meter samples on the MeterValuePeriodic path.
Out of scope (explicit follow-up split): the pre-authorization EVConnected state + triggerReason = CablePluggedIn before Started (ties into TxStartPoint semantics) — file a companion issue if this lands clean. A Sample.Periodic reading emitted while suspended is also out of scope.
Suspending outside an active transaction (no session) is inert — no panic.
The builder is pure and its output is schema-validated against the bundled 2.0.1 FINAL TransactionEvent JSON Schema; an invalid shape surfaces as an outbound-schema Err, never a panic.
SuspendedEV → Charging (resume): one event, chargingState = Charging.
Charging → SuspendedEVSE → Charging: two events with the correct states in order.
Redundant transition (SuspendedEV → SuspendedEV): no event emitted.
Suspend with no active transaction: inert, no panic.
seqNo monotonic and strictly between the Started and Ended events; builder output passes the v201 schema validator.
chargingState round-trips (serde) as the exact PascalCase wire tokens SuspendedEV / SuspendedEVSE.
Acceptance criteria
A V201-only driver hook drives a connector Charging ↔ SuspendedEV ↔ SuspendedEVSE and originates one schema-valid TransactionEvent(Updated, triggerReason=ChargingStateChanged) per real transition, carrying the transitioned chargingState.
Pure builder with a schema-validity test for each of SuspendedEV, SuspendedEVSE, and the Charging resume.
Redundant / no-transition and no-active-transaction cases are inert (no event, no seqNo burned, no panic).
seqNo continues the transaction's monotonic stream; Ended still closes with Idle.
A 1.6J charge point refuses the hook with OcppError::NotSupported (matches the other v201-only hooks).
cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test --workspace all green.
Reference
mobilityhouse/ocpp is protocol-only, so the behavior ported is the Charging Station side; the wire semantics are pinned by the ported types:
States & triggers: ocpp/v201/enums.py — ChargingStateEnumType (Charging, EVConnected, SuspendedEV, SuspendedEVSE, Idle) and TriggerReasonEnumType.charging_state_changed = "ChargingStateChanged" (plus cable_plugged_in for the deferred follow-up).
M7 — OCPP 2.0.1. Labels: port, m7. Small (one builder + one driver hook + tests; well under the ~500-LOC target).
🌙 Filed by the nightly dev during issue grooming (2026-09-12) — with #574/#573 now in-flight as PR #575/#576, the M7 earliest-incomplete queue drops toward fewer than 3 unclaimed well-scoped port issues once they merge. This is a genuine, code-verified gap (the simulator constructs only Charging/Idle, never a ChargingStateChanged suspend/resume transition), not queue padding. Direction for the broader M7→M8 track remains tracked in #256.
Context
The v201 transaction lifecycle in
crates/ocpp-cp/src/v201_transaction.rscurrently reports only two charging states across an entire session:ChargingonStarted/UpdatedandIdleonEnded.transaction_event_updated()hardcodeschargingState = ChargingandtriggerReason = MeterValuePeriodic(verified —v201_transaction.rs:258–289). A simulator-wide inventory confirms the gap:ChargingStateEnumType::{SuspendedEV, SuspendedEVSE, EVConnected}andTriggerReasonEnumType::ChargingStateChangedare never constructed anywhere incrates/ocpp-cp.Real charging stations emit an interim
TransactionEvent(Updated)withtriggerReason = ChargingStateChangedwhenever the session's charging state transitions — the EV pauses draw (SuspendedEV), the EVSE/CSMS pauses delivery (e.g. a 0 A charging profile / load management →SuspendedEVSE), or charging resumes (Charging). Without these, a downstream CSMS/CDR sees a session that is "always charging" until it abruptly ends — not faithful to OCPP 2.0.1 Part 2, and it hides the suspend semantics that smart-charging conformance exercises.Scope (small slice)
Charging ↔ SuspendedEV ↔ SuspendedEVSE, each emitted as oneTransactionEvent(Updated)withtriggerReason = ChargingStateChangedand the newtransactionInfo.chargingState.transaction_event_charging_state_changed(session, seq_no, new_state, timestamp)), plus a V201-only driver hook on the simulator to drive a connector intoSuspendedEV/SuspendedEVSEand back toCharging— the same opt-in behavior-injection pattern as the monitor-trip (port(m7): CP simulator 2.0.1 — emit NotifyEvent when a CSMS-installed variable monitor trips #545) / charging-limit hooks.seqNocontinues the transaction's monotonic stream (strictly betweenStartedandEnded); noidToken(already authorized); a pure state-change event carrieschargingState, not aSample.Periodicreading — leave meter samples on theMeterValuePeriodicpath.EVConnectedstate +triggerReason = CablePluggedInbeforeStarted(ties intoTxStartPointsemantics) — file a companion issue if this lands clean. ASample.Periodicreading emitted while suspended is also out of scope.Failure modes / trust boundary
seqNoburned (mirrors the hysteresis guarantee in port(m7): CP simulator 2.0.1 — auto-trip a variable monitor when an inbound SetVariables write crosses its threshold #573).TransactionEventJSON Schema; an invalid shape surfaces as an outbound-schemaErr, never a panic.call()— noRemoteCommandre-entrancy concern, unlike port(m7): CP simulator 2.0.1 — auto-trip a variable monitor when an inbound SetVariables write crosses its threshold #573.Test matrix
Charging → SuspendedEV: oneUpdatedevent,chargingState = SuspendedEV,triggerReason = ChargingStateChanged,seqNo= next.SuspendedEV → Charging(resume): one event,chargingState = Charging.Charging → SuspendedEVSE → Charging: two events with the correct states in order.SuspendedEV → SuspendedEV): no event emitted.seqNomonotonic and strictly between theStartedandEndedevents; builder output passes the v201 schema validator.chargingStateround-trips (serde) as the exact PascalCase wire tokensSuspendedEV/SuspendedEVSE.Acceptance criteria
Charging ↔ SuspendedEV ↔ SuspendedEVSEand originates one schema-validTransactionEvent(Updated, triggerReason=ChargingStateChanged)per real transition, carrying the transitionedchargingState.SuspendedEV,SuspendedEVSE, and theChargingresume.seqNoburned, no panic).seqNocontinues the transaction's monotonic stream;Endedstill closes withIdle.OcppError::NotSupported(matches the other v201-only hooks).cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.Reference
mobilityhouse/ocpp is protocol-only, so the behavior ported is the Charging Station side; the wire semantics are pinned by the ported types:
ocpp/v201/enums.py—ChargingStateEnumType(Charging,EVConnected,SuspendedEV,SuspendedEVSE,Idle) andTriggerReasonEnumType.charging_state_changed = "ChargingStateChanged"(pluscable_plugged_infor the deferred follow-up).ocpp/v201/call.pyclass TransactionEvent;ocpp/v201/datatypes.pyTransactionType.charging_state.crates/ocpp-cp/src/v201_transaction.rs(builders),crates/ocpp-types/src/v201/enums.rs:230(ChargingStateEnumType),:206(ChargingStateChanged).Milestone / labels
M7 — OCPP 2.0.1. Labels:
port,m7. Small (one builder + one driver hook + tests; well under the ~500-LOC target).🌙 Filed by the nightly dev during issue grooming (2026-09-12) — with #574/#573 now in-flight as PR #575/#576, the M7 earliest-incomplete queue drops toward fewer than 3 unclaimed well-scoped port issues once they merge. This is a genuine, code-verified gap (the simulator constructs only
Charging/Idle, never aChargingStateChangedsuspend/resume transition), not queue padding. Direction for the broader M7→M8 track remains tracked in #256.