feat(ocpp-cp): emit v201 ReservationStatusUpdate on reservation expiry/removal (M7) - #548
Open
duyhuynh-vn wants to merge 1 commit into
Open
feat(ocpp-cp): emit v201 ReservationStatusUpdate on reservation expiry/removal (M7)#548duyhuynh-vn wants to merge 1 commit into
duyhuynh-vn wants to merge 1 commit into
Conversation
…y/removal (M7) Wire the CP→CSMS half that closes the OCPP 2.0.1 reservation loop opened by ReserveNow (#483) and CancelReservation (#484): when a held reservation expires or is removed, a for_version(V201) station now sends ReservationStatusUpdate.req so the CSMS learns the slot is free again. - v201_command: pure `v201_reservation_status_update(reservation_id, status)` builder + schema-validity test (both Expired/Removed, extreme reservationId). - RemoteCommand::V201ReservationStatusUpdate variant + send helper, queued off the inbound/timer path (no receive-loop re-entrancy, never blocks the timer). - arm_reservation_expiry gains a protocol_version arg and, on V201 only, queues Expired when the timer actually claims and frees the reservation. - V201 CancelReservation queues Removed when it tears down a still-held reservation; an unknown id (Rejected) queues nothing. 1.6J emits neither. The Expired emit is gated on the atomic still-held claim and Removed on the atomic freed removal, so a cancel-vs-expiry race reports exactly one of the two (the cancel also aborts the timer). Ports ocpp.v201.call.ReservationStatusUpdate. Closes #546 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01TMS4zMza6pdXDtQaZqZnFH
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
Wire the CP→CSMS half that closes the OCPP 2.0.1 reservation loop: when a held reservation expires or is removed, a
for_version(V201)Charging Station now sendsReservationStatusUpdate.reqso the CSMS learns the slot is free again. This completes the 2.0.1 reservation family alongside the already-wired inboundReserveNow(#483) andCancelReservation(#484). Advances M7 — OCPP 2.0.1.Closes #546
Real use case
A driver reserves EVSE 3 at a site for a 20-minute window and then never shows up. The station's auto-expiry timer fires when the
expiryDateTimepasses, frees the connector, and now also tells the back officeReservationStatusUpdate(Expired)— so the CSMS dashboard stops showing the bay as held and can offer it to the next driver. Separately, when an operator cancels a still-active reservation from the back office, the station acks theCancelReservationand reportsReservationStatusUpdate(Removed), closing the loop so the CSMS's view of which bays are held always matches the station's. Before this change the expiry timer freed the connector silently and the CSMS could keep a freed slot marked reserved indefinitely.What changed
crates/ocpp-cp/src/v201_command.rs— a pure, unit-testable builderv201_reservation_status_update(reservation_id, status) -> ReservationStatusUpdateRequest(the CP→CSMS request half; the.confis empty, so there is no response builder).reservation_idis only echoed, never parsed or indexed.crates/ocpp-cp/src/lib.rsRemoteCommand::V201ReservationStatusUpdate { reservation_id, status }+send_v201_reservation_status_update(...), queued off the inbound-CALL / timer path so the outbound CALL never re-enters the receive loop mid-dispatch and never blocks the auto-expiry task. Best-effort send (log-not-propagate): the slot is already freed locally, so a dropped update must not undo it. There is no in-flight store to clear — a single fire-and-forget notification, not a stream.arm_reservation_expirygains aprotocol_versionargument and, on V201 only, queuesExpiredwhen the timer actually claims and frees the reservation (still_held). The reservation/expiry machinery is otherwise shared verbatim between the twoReserveNowarms, so the version is threaded in rather than duplicating the timer body. 1.6J has no such message, so a 1.6J expiry only frees the connector.CancelReservationhandler queuesRemovedwhen it tears down a still-held reservation (freedwasSome); a cancel of an unknown id is alreadyRejectedand queues nothing.Failure modes / concurrency. The
Expiredemit is gated on the atomicstill_heldclaim (taken under thereservationswrite-lock) andRemovedon the atomicfreedremoval, so a cancel-vs-expiry race reports exactly one of the two: whichever handler claims the map entry first emits, the other sees it gone and no-ops. The cancel also aborts the pending timer, so the same reservation can never be double-reported.Trust boundary.
reservation_idis CP-side state (echoed, never parsed);statusis simulator-decided from the reservation lifecycle, never attacker input.What was ported
ocpp/v201/call.py—ReservationStatusUpdate(reservation_id, reservation_update_status).ocpp/v201/call_result.py— emptyReservationStatusUpdate.ocpp/v201/enums.py—ReservationUpdateStatusEnumType(Expired/Removed).The message type and enum were already ported and schema-validated (
crates/ocpp-messages/src/v201/reservation_status_update.rs,crates/ocpp-types/src/v201/enums.rs); this PR is the CP-side emitter wiring + builder + tests.Test plan
cargo fmt --all --check— clean.cargo clippy --all-targets -- -D warnings— clean.cargo test --workspace— green (1979 tests pass).v201_command.rs): the builder forwards id + status verbatim; everyReservationStatusUpdate.req(bothExpired/Removed, and extremereservationId0 / 1 / -1 / i32::MIN / i32::MAX) is OCPP 2.0.1 schema-valid and never panics.lib.rs): an acceptedCancelReservationof a held reservation queues exactly oneRemoved; a cancel of an unknown id queues none; a V201 auto-expiry frees the connector and queues exactly oneExpired; a 1.6J auto-expiry frees the connector but queues noReservationStatusUpdate(the version gate); a cancel disarms the pending timer so the reservation can never be double-reported (Removedonce, noExpired, timer gone from the store). The expiry tests arm with a pastexpiryDate(ttl 0) andawaitthe spawned timer handle, so they are deterministic — no wall-clock wait.Acceptance criteria
RemoteCommand::V201ReservationStatusUpdatevariant + emitter, queued off the inbound/timer path.ReservationStatusUpdate.reqbuilder inv201_commandwith a schema-validity test covering bothExpiredandRemoved.Expiredand frees the connector;CancelReservationof a held reservation emitsRemoved; cancel of an unknown id emits nothing.cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.Known gaps / notes
Removedis scoped to CSMS-initiated cancels of still-held reservations. The spec also allows a station to reportRemovedwhen a reservation is dropped for another local reason (e.g. the connector becomesUnavailable/Faultedwhile reserved). The simulator has no path that unilaterally faults a reserved connector today, so that trigger is left as a natural follow-up; theRemovedarm and its wire/schema coverage are already in place for it.ReservationStatusUpdatedoes not exist in 1.6J, so a 1.6J CP's reservation teardown emits neither status (asserted by a regression test).🤖 Generated with Claude Code
https://claude.ai/code/session_01TMS4zMza6pdXDtQaZqZnFH
Generated by Claude Code