11/**
22 * Failure detection — analyzes events and sessions for known failure patterns.
33 *
4- * 15 detection rules (v0.1 + v0.2 + v0.3):
4+ * 16 detection rules (v0.1 + v0.2 + v0.3):
55 *
66 * v0.1:
77 * 1. FAILED_AUTHORIZATION — Authorize response with idTagInfo.status = "Invalid"
2323 * 13. HEARTBEAT_INTERVAL_VIOLATION — heartbeat intervals deviate >50% from expected
2424 * 14. METER_VALUE_ANOMALY — non-monotonic or negative meter readings
2525 * 15. UNRESPONSIVE_CSMS — Call with no matching CallResult or CallError
26+ * 16. REPEATED_BOOT_NOTIFICATION — 2+ BootNotification calls within 5 minutes
2627 *
2728 * @see ADR-0003
2829 */
@@ -134,6 +135,13 @@ const SUGGESTED_STEPS: Record<FailureCode, string[]> = {
134135 'Check if the CSMS crashed or restarted during the session' ,
135136 'Inspect the network path between station and CSMS for packet loss' ,
136137 ] ,
138+ REPEATED_BOOT_NOTIFICATION : [
139+ 'Check whether the station rebooted unexpectedly' ,
140+ 'Review station power and network stability during the boot window' ,
141+ 'Inspect station firmware logs for watchdog resets or startup failures' ,
142+ 'Verify the CSMS accepts the BootNotification and returns a valid interval' ,
143+ 'Contact the station vendor if repeated boots persist' ,
144+ ] ,
137145} ;
138146
139147const SEVERITY : Record < FailureCode , FailureSeverity > = {
@@ -152,6 +160,7 @@ const SEVERITY: Record<FailureCode, FailureSeverity> = {
152160 HEARTBEAT_INTERVAL_VIOLATION : 'info' ,
153161 METER_VALUE_ANOMALY : 'warning' ,
154162 UNRESPONSIVE_CSMS : 'critical' ,
163+ REPEATED_BOOT_NOTIFICATION : 'warning' ,
155164} ;
156165
157166// ---------------------------------------------------------------------------
@@ -380,6 +389,7 @@ export function detectFailures(events: Event[], sessions: Session[]): Failure[]
380389 failures . push ( ...detectHeartbeatIntervalViolation ( events ) ) ;
381390 failures . push ( ...detectMeterValueAnomaly ( events , sessions ) ) ;
382391 failures . push ( ...detectUnresponsiveCsms ( events ) ) ;
392+ failures . push ( ...detectRepeatedBootNotification ( events ) ) ;
383393
384394 return failures ;
385395}
@@ -725,6 +735,9 @@ const SLOW_RESPONSE_THRESHOLD_MS = 10_000;
725735/** Deviation threshold for heartbeat interval violation: 50%. */
726736const HEARTBEAT_DEVIATION_THRESHOLD = 0.5 ;
727737
738+ /** Time window for repeated BootNotification calls: 5 minutes. */
739+ const REPEATED_BOOT_NOTIFICATION_WINDOW_MS = 5 * 60 * 1000 ;
740+
728741/**
729742 * Rule 11: SUSPICIOUS_SESSION_DURATION
730743 * Detects sessions that are suspiciously short (< 60s) or long (> 24h).
@@ -988,3 +1001,63 @@ function detectUnresponsiveCsms(events: Event[]): Failure[] {
9881001
9891002 return failures ;
9901003}
1004+
1005+ /**
1006+ * Rule 16: REPEATED_BOOT_NOTIFICATION
1007+ * Detects when a station sends 2+ BootNotification Calls within 5 minutes.
1008+ */
1009+ function detectRepeatedBootNotification ( events : Event [ ] ) : Failure [ ] {
1010+ const failures : Failure [ ] = [ ] ;
1011+
1012+ const bootEvents = events
1013+ . filter (
1014+ ( event ) =>
1015+ event . messageType === 'Call' &&
1016+ event . action === 'BootNotification' &&
1017+ event . timestamp !== null ,
1018+ )
1019+ . sort ( ( a , b ) => ( a . timestamp as number ) - ( b . timestamp as number ) ) ;
1020+
1021+ for ( let i = 0 ; i < bootEvents . length ; i ++ ) {
1022+ const firstBoot = bootEvents [ i ] ;
1023+ if ( ! firstBoot || firstBoot . timestamp === null ) continue ;
1024+
1025+ const repeatedBoots = [ firstBoot ] ;
1026+ let j = i + 1 ;
1027+
1028+ while ( j < bootEvents . length ) {
1029+ const nextBoot = bootEvents [ j ] ;
1030+ if ( ! nextBoot || nextBoot . timestamp === null ) {
1031+ j ++ ;
1032+ continue ;
1033+ }
1034+
1035+ if ( nextBoot . timestamp - firstBoot . timestamp > REPEATED_BOOT_NOTIFICATION_WINDOW_MS ) {
1036+ break ;
1037+ }
1038+
1039+ repeatedBoots . push ( nextBoot ) ;
1040+ j ++ ;
1041+ }
1042+
1043+ if ( repeatedBoots . length >= 2 ) {
1044+ const windowSeconds = Math . round (
1045+ ( ( repeatedBoots [ repeatedBoots . length - 1 ] ?. timestamp ?? firstBoot . timestamp ) -
1046+ firstBoot . timestamp ) /
1047+ 1000 ,
1048+ ) ;
1049+
1050+ failures . push ( {
1051+ code : 'REPEATED_BOOT_NOTIFICATION' ,
1052+ description : `${ repeatedBoots . length } BootNotification calls detected within ${ windowSeconds } s — station may be rebooting repeatedly or failing startup` ,
1053+ severity : SEVERITY . REPEATED_BOOT_NOTIFICATION ,
1054+ eventIds : repeatedBoots . map ( ( event ) => event . id ) ,
1055+ suggestedSteps : SUGGESTED_STEPS . REPEATED_BOOT_NOTIFICATION ,
1056+ } ) ;
1057+
1058+ i = j - 1 ;
1059+ }
1060+ }
1061+
1062+ return failures ;
1063+ }
0 commit comments