Skip to content

Commit 7a0b42f

Browse files
committed
fix(core): track STATUS_TRANSITION_VIOLATION status per connector
The rule validated connector status transitions against a single previous status shared across all connectors, so on a multi-connector station an interleaved notification from a different connector (connector 1 going Available while connector 2 goes Finishing) was reported as an invalid transition. Status is now tracked per connectorId (connectorId 0, the charge point as a whole, forms its own series), and each transition is validated only within one connector's series. Genuine per-connector violations are still detected, so the scenario corpus and conformance contract are unchanged. Adds three regression tests. Reported by shiv3 from the ocpp-cp-simulator integration.
1 parent ac73cbb commit 7a0b42f

4 files changed

Lines changed: 81 additions & 20 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ocpp-debugkit/toolkit': patch
3+
---
4+
5+
Fix `STATUS_TRANSITION_VIOLATION` false positives (#128). The rule validated connector status transitions against a single previous status shared across all connectors, so on a multi-connector station an interleaved notification from a different connector (for example connector 1 going `Available` while connector 2 goes `Finishing`) was reported as an invalid transition. Status is now tracked per `connectorId` (connectorId 0, the charge point as a whole, forms its own series), and each transition is validated only within one connector's series. Reported by shiv3 from the ocpp-cp-simulator integration.

‎CURRENT_STATE.md‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44
55
## Current Version
66

7-
`0.4.0`, Open OCPP Trace Interop (published 2026-07-17). A `0.4.1` patch is in
8-
progress (METER_VALUE_ANOMALY false-positive fix, #127).
7+
`0.4.1`, METER_VALUE_ANOMALY fix (published 2026-07-22). A `0.4.2` patch is in
8+
progress (STATUS_TRANSITION_VIOLATION per-connector fix, #128).
99

1010
## Active Milestone
1111

12-
**v0.4.1 - detection correctness patch, then v0.5.0 (OCPP 2.0.1)**
12+
**v0.4.x - detection correctness patches, then v0.5.0 (OCPP 2.0.1)**
1313

1414
v0.4.0 shipped the Open OCPP Trace interop: the toolkit reads and writes the
1515
shared v1.1 interchange format (input adapter #121, exporter + `convert` CLI
16-
#122), checked against the specification's conformance fixtures. **0.4.1**
17-
fixes a false-positive bug shiv3 found from the simulator integration: the
18-
`METER_VALUE_ANOMALY` rule flattened all measurands and connectors into one
19-
series (#127). Next milestone is **v0.5.0 (OCPP 2.0.1)**, where the same
20-
per-connector/EVSE model this fix introduces is required across detection.
16+
#122), checked against the specification's conformance fixtures. Two
17+
false-positive bugs shiv3 found from the simulator integration are being
18+
fixed: `METER_VALUE_ANOMALY` measurand/connector flattening (#127, shipped in
19+
0.4.1) and `STATUS_TRANSITION_VIOLATION` global status tracking (#128, in
20+
0.4.2). Next milestone is **v0.5.0 (OCPP 2.0.1)**, where the per-connector /
21+
EVSE model these fixes introduce is required across detection.
2122

2223
## What's Done
2324

@@ -30,8 +31,16 @@ per-connector/EVSE model this fix introduces is required across detection.
3031
- ✅ Eliminates false positives on multi-measurand samples and multi-connector
3132
stations; genuine energy-register anomalies still detected (meter-anomaly
3233
scenario and conformance contract unchanged); 3 regression tests added
33-
- Companion issue drafted for the same connector-blindness in
34-
`STATUS_TRANSITION_VIOLATION` (rule 8)
34+
- Sibling connector-blindness in `STATUS_TRANSITION_VIOLATION` fixed under #128
35+
36+
### STATUS_TRANSITION_VIOLATION per-connector fix (0.4.2, Issue #128)
37+
38+
- ✅ Rule 8 now tracks the previous status per `connectorId` (connectorId 0,
39+
the whole charge point, forms its own series) and validates transitions only
40+
within one connector's series, instead of one global sequence
41+
- ✅ Eliminates false violations on multi-connector stations; genuine
42+
per-connector violations still detected (status-transition-violation scenario
43+
and conformance contract unchanged); 3 regression tests added
3544

3645
### GitHub Infrastructure
3746

‎packages/toolkit/src/core/detection.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,46 @@ describe('detectFailures', () => {
664664
const failures = detectFailures(events, sessions);
665665
expect(failures.some((f) => f.code === 'STATUS_TRANSITION_VIOLATION')).toBe(false);
666666
});
667+
668+
// Regression tests for the per-connector tracking fix (#128).
669+
const statusEvent = (id: string, connectorId: number, status: string, ts: number): Event =>
670+
makeEvent(id, id, 'Call', 'StatusNotification', { connectorId, status }, ts);
671+
672+
it('does not flag across connectors when interleaved statuses only look invalid globally (#128)', () => {
673+
const events = [
674+
statusEvent('e1', 1, 'Charging', 0),
675+
statusEvent('e2', 2, 'Available', 500),
676+
statusEvent('e3', 1, 'Finishing', 1000),
677+
];
678+
const failures = detectFailures(events, buildSessionTimeline(events));
679+
expect(failures.some((f) => f.code === 'STATUS_TRANSITION_VIOLATION')).toBe(false);
680+
});
681+
682+
it('still flags a genuine per-connector violation when another connector is interleaved', () => {
683+
const events = [
684+
statusEvent('e1', 1, 'Available', 0),
685+
statusEvent('e2', 2, 'Charging', 500),
686+
statusEvent('e3', 1, 'Finishing', 1000),
687+
];
688+
const violations = detectFailures(events, buildSessionTimeline(events)).filter(
689+
(f) => f.code === 'STATUS_TRANSITION_VIOLATION',
690+
);
691+
expect(violations).toHaveLength(1);
692+
expect(violations[0]?.eventIds).toEqual(['e1', 'e3']);
693+
});
694+
695+
it('tracks connectorId 0 (whole charge point) as its own series', () => {
696+
const events = [
697+
statusEvent('e1', 0, 'Available', 0),
698+
statusEvent('e2', 1, 'Available', 500),
699+
statusEvent('e3', 0, 'Finishing', 1000),
700+
];
701+
const violations = detectFailures(events, buildSessionTimeline(events)).filter(
702+
(f) => f.code === 'STATUS_TRANSITION_VIOLATION',
703+
);
704+
expect(violations).toHaveLength(1);
705+
expect(violations[0]?.eventIds).toEqual(['e1', 'e3']);
706+
});
667707
});
668708

669709
describe('DIAGNOSTICS_FAILURE', () => {

‎packages/toolkit/src/core/detection.ts‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -613,35 +613,42 @@ const VALID_TRANSITIONS: Record<string, Set<string>> = {
613613
function detectStatusTransitionViolation(events: Event[]): Failure[] {
614614
const failures: Failure[] = [];
615615

616-
// Collect all StatusNotification statuses in order
617616
const statusEvents = events.filter(
618617
(e) => e.messageType === 'Call' && e.action === 'StatusNotification',
619618
);
620619

621-
let prevStatus: string | null = null;
622-
let prevEvent: Event | null = null;
620+
// Each connector has an independent status machine, so a transition is only
621+
// valid or invalid relative to the same connector's previous status. Track
622+
// the previous status per connectorId; connectorId 0 refers to the charge
623+
// point as a whole (OCPP 1.6) and forms its own series, and a missing
624+
// connectorId is bucketed under 'unknown'. Comparing statuses across
625+
// connectors (the previous behavior) produced false violations on
626+
// multi-connector stations.
627+
const previousByConnector = new Map<number | string, { status: string; event: Event }>();
623628

624629
for (const event of statusEvents) {
625-
const payload = event.payload as { status?: unknown };
630+
const payload = event.payload as { status?: unknown; connectorId?: unknown };
626631
const status = payload?.status;
627632

628633
if (typeof status !== 'string' || !VALID_CONNECTOR_STATUSES.has(status)) continue;
629634

630-
if (prevStatus !== null && prevEvent !== null) {
631-
const allowed: Set<string> | undefined = VALID_TRANSITIONS[prevStatus];
635+
const connectorId = typeof payload?.connectorId === 'number' ? payload.connectorId : 'unknown';
636+
const previous = previousByConnector.get(connectorId);
637+
638+
if (previous) {
639+
const allowed: Set<string> | undefined = VALID_TRANSITIONS[previous.status];
632640
if (allowed && !allowed.has(status)) {
633641
failures.push({
634642
code: 'STATUS_TRANSITION_VIOLATION',
635-
description: `Connector status transition from "${prevStatus}" to "${status}" is not a valid OCPP 1.6 transition (messageId: ${event.messageId})`,
643+
description: `Connector status transition from "${previous.status}" to "${status}" is not a valid OCPP 1.6 transition (messageId: ${event.messageId})`,
636644
severity: SEVERITY.STATUS_TRANSITION_VIOLATION,
637-
eventIds: [prevEvent.id, event.id],
645+
eventIds: [previous.event.id, event.id],
638646
suggestedSteps: SUGGESTED_STEPS.STATUS_TRANSITION_VIOLATION,
639647
});
640648
}
641649
}
642650

643-
prevStatus = status;
644-
prevEvent = event;
651+
previousByConnector.set(connectorId, { status, event });
645652
}
646653

647654
return failures;

0 commit comments

Comments
 (0)