feat(ocpp-cp): auto-trip a v201 variable monitor on a threshold-crossing SetVariables write (M7) - #576
Merged
Conversation
…ing SetVariables write (M7) Closes #573. Completes the OCPP 2.0.1 variable-monitoring loop that #570 (the trip_variable_monitor injection seam) left half-done: a monitor now trips autonomously when a CSMS SetVariables write moves a monitored variable's Actual value across an installed threshold, or by >= a configured delta. Because the trip is raised from inside an inbound-CALL handler, it is drained through the RemoteCommand queue (new V201MonitorTrip variant) rather than emitted inline, so the SetVariablesResponse CALLRESULT is flushed before the outbound NotifyEvent (no receive-loop re-entrancy) — the same discipline NotifyReport / NotifyMonitoringReport already follow. - V201DeviceModel::monitors_tripped_by_write selects only the write-driven monitors a numeric crossing hits (Upper/LowerThreshold cross either direction; Delta on a single-write jump; Periodic never). Hysteresis falls out of crossing detection; non-numeric / NaN / inf values cross nothing but are still reported verbatim. - The SetVariables handler captures the prior Actual value and enqueues one V201MonitorTrip per crossed variable; rejected / unknown / non-Actual / no-op-rewrite writes trip nothing. - The #545 NotifyEvent emitter core is extracted into ChargePoint::emit_monitor_notify_event, shared by the inline seam and the new consumer path — no duplicated trigger/correlation/seqNo logic. Tests: pure device-model selector + predicate + parse tests; handler tests via v201_drain_commands covering all threshold/delta/hysteresis/rejected/ periodic/no-op/non-numeric arms; and an end-to-end mock-CSMS test proving the NotifyEvent is emitted after the response, off the command queue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSdgXz4N5iiSh1UiXPi7za
6 tasks
duyhuynh-vn
pushed a commit
that referenced
this pull request
Sep 12, 2026
Resolve a conflict in crates/ocpp-cp/src/lib.rs test module: #576's SetVariables auto-trip tests and this PR's NotifyReport tbc-paging tests were both appended at the same anchor, and git interleaved them on an identical ChargePoint::new(..) block shared by both tests. Reconstructed each test intact and de-interleaved; both now run and pass. cargo fmt --check, clippy --all-targets -D warnings, and test --workspace all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PeYP9bfNgyvmXsD7aLqFpG
5 tasks done
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
Completes the OCPP 2.0.1 variable-monitoring loop that #570 (issue #545) left half-done: a monitor now trips autonomously when a CSMS
SetVariableswrite moves a monitored variable'sActualvalue across an installed threshold (or by ≥ a configured delta), emitting aNotifyEventoff the command queue. Advances M7 — OCPP 2.0.1.Closes #573Real use case
A CSMS installs an upper-threshold monitor on a charging station's
OCPPCommCtrlr.HeartbeatInterval(alert if it ever exceeds 900 s) viaSetVariableMonitoring. Later, an operator (or a misconfiguration) pushes a new heartbeat interval of 1000 s withSetVariables. On a real station the monitor fires the moment that write lands, so the CSMS learns — via an unsolicitedNotifyEvent(triggerAlerting) — that a monitored value has entered its alarm band, without having to poll. Before this change the simulator stored the new value silently and the monitor never fired, so a CSMS integration test that relies on threshold alerting had nothing to observe. Now the write auto-originates the correlatedNotifyEvent, and a subsequent write back below the threshold fires the leaving-band event — exactly as an operator watching that alarm would expect.What changed
crates/ocpp-cp/src/v201_device_model.rs— new pureV201DeviceModel::monitors_tripped_by_write(component, variable, previous, new)selects only the write-driven monitors a numeric crossing hits, id-sorted:Upper/LowerThreshold→ trips on a crossing in either direction (opposite sides of the threshold); hysteresis falls out for free (a same-band write crosses nothing).Delta→ trips when the single write jumps by ≥ the configured magnitude.Periodic/PeriodicClockAligned→ never selected (time-driven; compile-exhaustive match).previous/newthat is not a finitef64(non-numeric,NaN,inf) crosses nothing — the opaque value is still reported verbatim. Shared key-building extracted intomonitor_lookup_key;MonitorEntry::to_trippedprojection shared withmonitors_for_variable.crates/ocpp-cp/src/lib.rs— theSetVariableshandler captures the priorActualvalue under the write lock and, on an accepted, value-changingActualwrite, enqueues one newRemoteCommand::V201MonitorTrip { monitors, actual_value }per crossed variable. Failure modes / trust boundary: rejected / unknown / non-Actual/ no-op-rewrite writes trip nothing; caller values stay opaque and thread to the wire verbatim (oversized → outbound-schemaErr, never a panic); no match → nothing enqueued, noseqNoburned; a gone consumer drops the trip best-effort after the honest CALLRESULT.SetVariablesResponseCALLRESULT is flushed before the outboundNotifyEvent— no receive-loop re-entrancy, the same discipline asNotifyReport/NotifyMonitoringReport.NotifyEventbuild-and-send core is extracted intoChargePoint::emit_monitor_notify_event, reused by bothtrip_variable_monitor(inline injection seam) and the newV201MonitorTripconsumer — trigger derivation,CustomMonitortagging,variableMonitoringIdcorrelation, and the monotonicseqNo/eventIdstreams live in one place (no duplicated trip logic).What was ported
mobilityhouse/ocpp is protocol-only, so the wire types are the port surface and the when-to-fire behavior is the Charging Station's:
ocpp/v201/call.py—NotifyEvent,SetVariables.ocpp/v201/datatypes.py—EventDataType,VariableMonitoringType.ocpp/v201/enums.py—MonitorEnumType,EventTriggerEnumType,EventNotificationEnumType.Test plan
cargo fmt --check✅cargo clippy --all-targets -- -D warnings✅cargo test --workspace✅New tests:
monitors_tripped_by_writeselection (only crossed write-driven kinds, id-sorted, periodic excluded, non-numeric ignored);monitor_write_tripspredicate (both threshold directions, on-threshold boundary, delta boundary, time-driven excluded);parse_finite_f64rejectsNaN/inf/non-numeric.v201_drain_commands): upper-threshold fires on each crossing with hysteresis; lower-threshold symmetric; delta fires on a jump but not a sub-delta write; multiple monitors ride one id-sorted trip; rejected write / non-monitored write / periodic monitor / no-op rewrite / non-numeric value all trip nothing.SetVariableswrite is answeredAcceptedand the correlatedNotifyEvent(trigger: Alerting,CustomMonitor,variableMonitoringId,seqNo 0) follows on the wire — proving the event is emitted after the response, off the command queue.Acceptance criteria
SetVariableswrite that crosses an installed threshold/delta monitor auto-originates one schema-validNotifyEventper matched variable, via theRemoteCommandqueue (not inline).EventDataTypecorrelation, monotonicseqNo+eventId,CustomMonitornotification type) — no duplicated trip logic.Periodic/PeriodicClockAlignedmonitors are not affected; rejected and non-monitored writes never trip; hysteresis prevents same-band re-fire.Err, no panic.cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.Known gaps / notes
NotifyEventremains single-page (as in port(m7): CP simulator 2.0.1 — emit NotifyEvent when a CSMS-installed variable monitor trips #545); multi-pagetbcchunking for large event sets is tracked separately (see port(m7): CP simulator 2.0.1 — chunk NotifyReport / NotifyMonitoringReport across multiple pages (tbc paging) #574's notes) and out of scope here.🤖 Generated with Claude Code
https://claude.ai/code/session_01BSdgXz4N5iiSh1UiXPi7za
Generated by Claude Code