Files
2025-12-02 16:27:21 +00:00

151 lines
5.7 KiB
Plaintext

// ============================================================
// ANOMALY DETECTOR
// Detects production anomalies in real-time
// ============================================================
const cycle = msg.cycle || {};
const kpis = msg.kpis || {};
const activeOrder = global.get("activeWorkOrder") || {};
// Must have active work order to detect anomalies
if (!activeOrder.id) {
return null;
}
const theoreticalCycleTime = Number(activeOrder.cycleTime) || 0;
const now = Date.now();
// Get or initialize anomaly tracking state
let anomalyState = global.get("anomalyState") || {
lastCycleTime: now,
activeStoppageEvent: null
};
const detectedAnomalies = [];
// ============================================================
// 1. SLOW CYCLE DETECTION
// Trigger: Actual cycle time > 1.5x theoretical
// ============================================================
if (theoreticalCycleTime > 0) {
const timeSinceLastCycle = now - anomalyState.lastCycleTime;
const actualCycleTime = timeSinceLastCycle / 1000; // Convert to seconds
const threshold = theoreticalCycleTime * 1.5;
if (actualCycleTime > threshold && anomalyState.lastCycleTime > 0) {
const deltaPercent = ((actualCycleTime - theoreticalCycleTime) / theoreticalCycleTime) * 100;
// Determine severity
let severity = 'warning';
if (actualCycleTime > theoreticalCycleTime * 2.0) {
severity = 'critical'; // 100%+ slower
}
detectedAnomalies.push({
anomaly_type: 'slow-cycle',
severity: severity,
title: `Slow Cycle Detected`,
description: `Cycle took ${actualCycleTime.toFixed(1)}s (${deltaPercent.toFixed(0)}% slower than expected ${theoreticalCycleTime}s)`,
data: {
actual_cycle_time: actualCycleTime,
theoretical_cycle_time: theoreticalCycleTime,
delta_percent: Math.round(deltaPercent),
threshold_multiplier: actualCycleTime / theoreticalCycleTime
},
kpi_snapshot: {
oee: kpis.oee || 0,
availability: kpis.availability || 0,
performance: kpis.performance || 0,
quality: kpis.quality || 0
},
work_order_id: activeOrder.id,
cycle_count: cycle.cycles || 0,
timestamp: now
});
node.warn(`[ANOMALY] Slow cycle: ${actualCycleTime.toFixed(1)}s (expected ${theoreticalCycleTime}s)`);
}
}
// ============================================================
// 2. PRODUCTION STOPPAGE DETECTION
// Trigger: No cycle in > 3x theoretical cycle time
// ============================================================
if (theoreticalCycleTime > 0) {
const timeSinceLastCycle = now - anomalyState.lastCycleTime;
const stoppageThreshold = theoreticalCycleTime * 3 * 1000; // Convert to ms
// If we have an active stoppage event and a new cycle arrived, resolve it
if (anomalyState.activeStoppageEvent) {
// Cycle resumed - mark stoppage as resolved
anomalyState.activeStoppageEvent.resolved_at = now;
anomalyState.activeStoppageEvent.auto_resolved = true;
anomalyState.activeStoppageEvent.status = 'resolved';
const stoppageDuration = (now - anomalyState.activeStoppageEvent.timestamp) / 1000;
node.warn(`[ANOMALY] Production resumed after ${stoppageDuration.toFixed(0)}s stoppage`);
// Send resolution event
detectedAnomalies.push(anomalyState.activeStoppageEvent);
anomalyState.activeStoppageEvent = null;
}
// Check if production has stopped (only if no active stoppage event)
if (!anomalyState.activeStoppageEvent && timeSinceLastCycle > stoppageThreshold && anomalyState.lastCycleTime > 0) {
const stoppageSeconds = timeSinceLastCycle / 1000;
// Determine severity
let severity = 'warning';
if (stoppageSeconds > theoreticalCycleTime * 5) {
severity = 'critical'; // Stopped for 5x+ theoretical time
}
const stoppageEvent = {
anomaly_type: 'production-stopped',
severity: severity,
title: `Production Stoppage`,
description: `No cycles detected for ${stoppageSeconds.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,
data: {
stoppage_duration_seconds: Math.round(stoppageSeconds),
theoretical_cycle_time: theoreticalCycleTime,
last_cycle_timestamp: anomalyState.lastCycleTime,
threshold_multiplier: stoppageSeconds / theoreticalCycleTime
},
kpi_snapshot: {
oee: kpis.oee || 0,
availability: kpis.availability || 0,
performance: kpis.performance || 0,
quality: kpis.quality || 0
},
work_order_id: activeOrder.id,
cycle_count: cycle.cycles || 0,
timestamp: now,
status: 'active'
};
detectedAnomalies.push(stoppageEvent);
anomalyState.activeStoppageEvent = stoppageEvent;
node.warn(`[ANOMALY] Production stopped: ${stoppageSeconds.toFixed(0)}s since last cycle`);
}
}
// Update last cycle time for next iteration
anomalyState.lastCycleTime = now;
global.set("anomalyState", anomalyState);
// ============================================================
// OUTPUT
// ============================================================
if (detectedAnomalies.length > 0) {
node.warn(`[ANOMALY DETECTOR] Detected ${detectedAnomalies.length} anomaly/ies`);
return {
topic: "anomaly-detected",
payload: detectedAnomalies,
originalMsg: msg // Pass through original message for other flows
};
}
return null; // No anomalies detected