Context
With the inbound CSMS→CP handler wiring for OCPP 2.0.1 now essentially complete (all 39 CSMS-initiated commands are wired or in-flight), the remaining M7 work is the CP→CSMS-initiated async flows the simulator does not yet drive.
The variable-monitoring family is currently a dead-end: the CP accepts and stores monitors via SetVariableMonitoring (#498), ClearVariableMonitoring (#499), SetMonitoringBase (#504), SetMonitoringLevel (#503), and reports the configured set via GetMonitoringReport (#495) → NotifyMonitoringReport. But no monitor ever fires — the CP never emits a NotifyEvent.req. The monitor store in crates/ocpp-cp/src/v201_device_model.rs (monitors: HashMap<i32, MonitorEntry>) even carries a documented "a future emitter can honor it" seam comment for exactly this.
This is the direct async twin of the already-shipped NotifyReport (#487) and NotifyMonitoringReport (#495) streams — same off-CALL-path RemoteCommand queueing discipline.
Python reference
ocpp/v201/call.py → class NotifyEvent (generated_at, seq_no, event_data: List[EventDataType], tbc)
ocpp/v201/datatypes.py → class EventDataType (event_id, timestamp, trigger, actual_value, event_notification_type, component, variable, optional cause, tech_code, tech_info, cleared, transaction_id, variable_monitoring_id)
ocpp/v201/enums.py → EventTriggerEnumType, EventNotificationEnumType
Proposed design
Because a pure simulator has no naturally-changing variable values, the trip must be deterministically injected — matching the simulator's existing opt-in behavior-injection pattern (firmware fault injection, unlock_outcome, etc.):
- A test/sim hook (e.g.
trip_variable_monitor(component, variable, actual_value)) that looks up a matching monitor in the store and, if found, queues a RemoteCommand::V201NotifyEvent.
run_v201_notify_event emits a schema-valid NotifyEvent.req with correctly correlated variable_monitoring_id, the monitor's EventTriggerEnumType (Delta/Periodic/UpperThreshold/LowerThreshold derived from the monitor type), event_notification_type, component/variable, and a monotonically increasing seq_no.
- No matching monitor → no event queued (no-op), consumer-gone rolls the marker back (existing convention).
Acceptance criteria
Milestone: M7. Target ≤ ~500 LOC; if larger, ship the emitter + happy path and split threshold/delta derivation into a follow-up.
Filed by the nightly dev during Sunday issue grooming (2026-08-23). Direction tracked in #256.
Context
With the inbound CSMS→CP handler wiring for OCPP 2.0.1 now essentially complete (all 39 CSMS-initiated commands are wired or in-flight), the remaining M7 work is the CP→CSMS-initiated async flows the simulator does not yet drive.
The variable-monitoring family is currently a dead-end: the CP accepts and stores monitors via
SetVariableMonitoring(#498),ClearVariableMonitoring(#499),SetMonitoringBase(#504),SetMonitoringLevel(#503), and reports the configured set viaGetMonitoringReport(#495) →NotifyMonitoringReport. But no monitor ever fires — the CP never emits aNotifyEvent.req. The monitor store incrates/ocpp-cp/src/v201_device_model.rs(monitors: HashMap<i32, MonitorEntry>) even carries a documented "a future emitter can honor it" seam comment for exactly this.This is the direct async twin of the already-shipped
NotifyReport(#487) andNotifyMonitoringReport(#495) streams — same off-CALL-pathRemoteCommandqueueing discipline.Python reference
ocpp/v201/call.py→class NotifyEvent(generated_at,seq_no,event_data: List[EventDataType],tbc)ocpp/v201/datatypes.py→class EventDataType(event_id,timestamp,trigger,actual_value,event_notification_type,component,variable, optionalcause,tech_code,tech_info,cleared,transaction_id,variable_monitoring_id)ocpp/v201/enums.py→EventTriggerEnumType,EventNotificationEnumTypeProposed design
Because a pure simulator has no naturally-changing variable values, the trip must be deterministically injected — matching the simulator's existing opt-in behavior-injection pattern (firmware fault injection,
unlock_outcome, etc.):trip_variable_monitor(component, variable, actual_value)) that looks up a matching monitor in the store and, if found, queues aRemoteCommand::V201NotifyEvent.run_v201_notify_eventemits a schema-validNotifyEvent.reqwith correctly correlatedvariable_monitoring_id, the monitor'sEventTriggerEnumType(Delta/Periodic/UpperThreshold/LowerThreshold derived from the monitor type),event_notification_type,component/variable, and a monotonically increasingseq_no.Acceptance criteria
RemoteCommand::V201NotifyEventvariant +run_v201_notify_eventemitter, queued off the inbound path (never re-entering the receive loop mid-dispatch).NotifyEvent.req/EventDataTypepage builder inv201_commandwith a schema-validity test against the bundled FINAL JSON Schema.MonitorTypeto the correctEventTriggerEnumType.seq_noincrements across events; hostile/oversized injectedactual_valuenever panics (trust boundary — value is stringly reported, never parsed).cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test --workspaceall green.Milestone: M7. Target ≤ ~500 LOC; if larger, ship the emitter + happy path and split threshold/delta derivation into a follow-up.
Filed by the nightly dev during Sunday issue grooming (2026-08-23). Direction tracked in #256.