Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/diarpicu2022-commits/backend-AgroPulse/llms.txt

Use this file to discover all available pages before exploring further.

AgroPulse continuously monitors every sensor that has an active threshold configuration. Every 2 minutes, AnomalyDetectionService runs a scheduled sweep across all configured sensors and evaluates four distinct anomaly conditions. When a condition is first detected, an anomaly record is created and a notification is dispatched. When the condition clears on a later sweep, the anomaly is automatically resolved.
Each new anomaly also creates an Alert record that appears immediately in the alerts feed. You can view active and historical alerts via /api/alerts.

How the scheduler works

The detection sweep is triggered by a Spring @Scheduled annotation with a fixed delay of 120,000 milliseconds (2 minutes). The delay is measured from the end of the previous run, so a slow sweep does not cause overlapping executions. On each run, the service:
  1. Loads all SensorThreshold records where active = true
  2. Evaluates each sensor against its four anomaly types
  3. Creates new SensorAnomaly records for any newly detected conditions
  4. Sets resolvedAt on any anomaly whose condition has cleared
  5. Dispatches email and WhatsApp notifications for all unnotified anomalies

Sensor thresholds

Each sensor threshold record controls the parameters used for all four anomaly checks on that sensor.
FieldTypeUsed by
minValuedecimalOUT_OF_RANGE lower bound
maxValuedecimalOUT_OF_RANGE upper bound
noDataMinutesintegerMinutes of silence before NO_DATA fires
stuckMinutesintegerWindow size for the STUCK spread check
spikePercentdecimalPercentage change threshold for SPIKE

Anomaly types

Fires when the sensor’s most recent value falls below minValue or above maxValue. Either bound is optional—if minValue is null only the upper bound is checked, and vice versa.Condition: lastValue < minValue OR lastValue > maxValueExample message: Valor 38.50 fuera de rango [5.0 — 35.0]Alert level: CRITICALThis anomaly is skipped entirely if NO_DATA is currently active for the same sensor, since there is no reliable last value to compare.
Fires when the most recent reading timestamp is older than noDataMinutes minutes, or when no reading has ever been recorded for the sensor.Condition: MAX(timestamp) < now() - noDataMinutesExample message: Sin datos desde hace más de 30 minutosAlert level: CRITICALA NO_DATA anomaly suppresses the OUT_OF_RANGE, STUCK, and SPIKE checks for the same sensor on the same sweep, since those checks rely on recent reading data.
Fires when the last 20 readings within the stuckMinutes window show a spread (max − min) of less than 0.01. At least 3 readings must be present in the window for the check to activate.Condition: (MAX(value) - MIN(value)) < 0.01 across the last 20 readings in stuckMinutes window, with at least 3 readings presentExample message: Valor constante durante 60 minutos (posible daño en sensor)Alert level: WARNINGA spread below 0.01 across multiple readings usually means the sensor is frozen, disconnected, or physically damaged rather than reporting a legitimately stable environment.
Fires when the relative change between the two most recent readings exceeds spikePercent. The check is skipped if the previous reading’s absolute value is too close to zero (below 0.001) to avoid division errors.Condition: |reading[0] - reading[1]| / |reading[1]| > spikePercent / 100Example message: Cambio brusco: 22.10 → 38.75Alert level: WARNINGA spike may indicate a momentary sensor malfunction, an electrical surge, or a real rapid environmental change. Reviewing the reading history helps distinguish between these cases.

Auto-resolution

Anomaly resolution is fully automatic. On each sweep, if a previously detected anomaly condition is no longer true—for example, a value that was out of range has returned within bounds—the service sets resolvedAt to the current timestamp on the existing SensorAnomaly record. No new record is created. An anomaly is considered active as long as resolvedAt IS NULL. Resolved anomalies are kept in the database for historical review.
// Resolution logic in AnomalyDetectionService
existing.ifPresent(a -> {
    a.setResolvedAt(LocalDateTime.now());
    anomalyRepository.save(a);
});
Only one open anomaly of each type is allowed per sensor at a time. If the condition is already active (an unresolved record exists), no duplicate is created.

Build docs developers (and LLMs) love