Script icon
Docker icon
Get icon
If icon
Set icon
Log icon
SlackIncomingWebhook icon
Publish icon
RealtimeTrigger icon

Detect Sustained IoT Motor Health Anomalies and Request Safe Mode

Score MQTT motor telemetry, persist trends in Redis, detect sustained anomalies, alert Slack, and optionally request safe mode with dry-run safeguards.

Categories
DataInfrastructure

Use MQTT telemetry to monitor motors in robots, pumps, conveyors, fans and automated guided vehicles (AGVs). Kestra validates a generic metric contract, reads previous state from Redis, calculates weighted health and an exponential moving average (EMA), tracks sustained anomalies and recovery, notifies Slack, and optionally requests device-defined safe mode through MQTT.

This is an explainable condition-monitoring heuristic for predictive maintenance triage, not a trained failure predictor or a certified safety controller. Example thresholds are illustrative, not equipment ratings. Keep independent hardware protection and calibrate thresholds for each asset. Separate flows or namespaces are appropriate for different asset profiles.

Prerequisites

  • A compatible Kestra installation with MQTT, Redis, Slack, Python Script and Docker task-runner plugins. Validate against your installed versions.
  • A Docker-compatible engine accessible to Kestra workers; Python tasks use python:3.12-slim and install the kestra Python package. Image/package download access is needed, or use a prebuilt internal image with that package.
  • An MQTT 3.1.1 (default V3) or MQTT 5 broker accessible to the Kestra server and workers; a Redis service reachable by workers. Use authenticated TLS services in a deployed environment and configure the broker CA/trust store.
  • A Slack incoming webhook for live notifications. Dry runs do not need Slack.
  • Devices or an adapter must emit the contract below and independently enforce command expiry, device identity, command deduplication and their own safe mode.

Secrets (names only; never commit values)

  • MQTT_SERVER: broker URI, e.g. ssl://broker.example.com:8883; required by trigger and publisher. tcp:// is suitable only for an isolated local test broker.
  • MQTT_USERNAME: broker account used to read telemetry and publish commands.
  • MQTT_PASSWORD: broker account password. For an anonymous local test broker, define the username/password secrets as empty strings or remove both fields.
  • REDIS_URI: Redis connection URI including required authentication/database, e.g. rediss://user:password@redis.example.com:6379/0; required for persistence.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL; required only for live alerts and live execution-error notifications. On Kestra OSS, expose base64-encoded values through SECRET_ environment variables. Consult the secrets documentation for your deployment.

Telemetry contract (schema_version=1)

Publish UTF-8 JSON to devices/<device_id>/telemetry with retain=false:

{
  "schema_version": 1,
  "device_id": "pump-07",
  "timestamp": "2026-10-09T12:00:00Z",
  "operating_state": "RUNNING",
  "metrics": {
    "motor_temp_c": 74.0,
    "motor_current_a": 7.3,
    "vibration_mm_s": 6.2
  }
}

Replace the example timestamp with the current UTC time. Fields: device_id contains 1..64 letters, digits, underscores or hyphens; timestamp is UTC ISO 8601 ending in Z and strictly increasing per device; operating_state is RUNNING or IDLE; metrics supplies a finite JSON number for every configured profile metric. Additional numeric metrics are ignored. Timestamps include date, time and seconds, with at most 6 fractional digits. Synchronize device clocks. A duplicate or older timestamp is ignored, including QoS 1 redelivery. Do not reset the clock when restarting a device. An MQTT topic/device mismatch is rejected. IDLE resets streaks and suppresses new anomalies/commands; it does not clear a latched critical incident. Every configured metric is required even when idle. Normalize source telemetry in an adapter: motor_temp means degrees C only after mapping to motor_temp_c; normalized vibration is not vibration_mm_s.

Inputs

  • telemetry (JSON, default {}): manual payload using the contract; without an MQTT trigger, supply all required fields. Manual runs cannot publish commands.
  • dry_run (BOOL, default true): no Slack or MQTT writes, including errors; Redis is still read/written under a separate dry-run key. Simulated alerts advance their own cooldown; simulated commands do not advance command cooldown.
  • auto_remediate (BOOL, default false): opt into commands only with dry_run=false and a fresh MQTT sample. Changing this alone never publishes a command.
  • health_profile (JSON): default motor_temp_c high 60/80 weight 0.4, motor_current_a high 5/8 weight 0.3, vibration_mm_s high 4.5/7.1 weight 0.3. Supply 1..16 metrics. Each has exactly direction (high or low), warning, critical (finite numbers) and weight (>0). For high, warning < critical; for low, warning > critical. Metric names begin with a letter and have at most 64 letters/digits/underscores. Weights are normalized automatically. Add rpm for low-speed risk or battery_v for undervoltage with direction=low; remove metrics absent from your sensors. Match units and operating envelope.
  • detection_policy (JSON): supply all fields, with the following defaults: ema_alpha=0.35 (>0..1); anomaly_score=60; recovery_score=80 (0 <= anomaly_score < recovery_score <= 100); min_anomaly_samples=3 and min_recovery_samples=3 (integers 2..1000); min_anomaly_seconds=30; max_sample_gap_seconds=120; max_sample_age_seconds=300; max_future_skew_seconds=10 (0..60); alert_cooldown_seconds=900; command_cooldown_seconds=1800; command_ttl_seconds=30 (>0..300). Other durations must be positive seconds. Samples must satisfy both the count and duration; more than the minimum sample count may be needed at a higher reporting frequency.
  • state_ttl (DURATION, default P7D): positive Redis retention duration, e.g. P1D, P7D or PT12H (positive days/hours/minutes/seconds). Expiry loses streaks/cooldowns and starts a new history. Choose longer retention than cooldowns and typical device offline periods. MQTT executions use flow input defaults. Change defaults in the YAML to set unattended policy; manual overrides affect only that execution. Edit variables device_topic_root (devices), mqtt_subscriber_client_id (kestra-motor-health), mqtt_version (V3 or V5) and state_key_prefix (motor-health-v1) as needed. Each deployed subscriber requires a unique, stable client ID on its broker.

How detection works

Each metric's risk is clamp((value-warning)/(critical-warning), 0, 1), valid for both high and low profiles. Raw health is 100*(1-weighted mean risk). EMA health is alpha*raw+(1-alpha)*previous EMA. A configured critical metric, or both raw and EMA health <= anomaly_score, starts a RUNNING anomaly streak. A healthy raw sample breaks the streak even while EMA remembers a past spike. CRITICAL requires both the minimum consecutive sample count and elapsed event-time duration. At 15-second intervals, 3 anomalous samples span 30 seconds. Gaps over the limit, profile/policy changes and non-anomalous samples reset anomaly streaks. Profile/policy or operating-state changes also reset the EMA and recovery streak, preserving the critical latch and existing effect cooldowns. Redis expiry resets everything. A latched incident recovers only after min_recovery_samples consecutive RUNNING samples with no anomaly and EMA >= recovery_score. Trend describes a per-sample EMA change above 1 point, not a time-to-failure estimate.

Tasks and effects

  1. validate_telemetry checks the contract, policy, freshness and topic routing.
  2. load_previous_state reads Redis; a missing key starts a new history.
  3. score_health computes scores, streaks, status, pending alerts and cooldowns.
  4. process_accepted_sample persists health and branches to Slack/safe-mode tasks. Pending incident/recovery alerts survive delivery failures. Alert cooldown advances only after a successful webhook response or simulated alert. A subsequent fresh sample can retry a pending alert; no background retry runs when the device stops transmitting.
  5. consider_safe_mode requires a sustained current anomaly and command cooldown. It checks opt-in, dry-run and MQTT source, then rechecks freshness, publishes a non-retained command and records broker-publication time in Redis.
  6. errors logs failures and sends a generic Slack failure message in live mode. No recovery path issues a command to resume normal operation. Redis reads/writes use bounded retries; Slack and MQTT publication intentionally have no task retry, because network uncertainty can duplicate external effects. Slack acceptance/broker publication is not proof of operator/device action. A crash between external success and its Redis receipt can duplicate an effect on a later sample. Devices must deduplicate command_id and reject expired commands; Kestra cannot guarantee exactly-once control. Replaying the same publication task has the same command_id; different samples have different IDs.

Safe-mode contract

Topic: devices/<device_id>/commands; JSON schema_version=1 with command=safe_mode, command_id, device_id, reason, telemetry_timestamp, issued_at and expires_at. The device defines safe mode (for example reduced load or controlled stop). Expiry is the earlier of now+command_ttl_seconds and the telemetry freshness deadline. Commands are never retained; a disconnected device must not act on an old command when it reconnects. Authentication/topic ACLs and device-side expiry/identity checks are required. Acknowledgement handling is outside this flow.

Quick start

  1. Set secrets, install plugins, configure Docker access, and calibrate the profile.
  2. Use a distinct subscriber client ID and a low-rate device telemetry topic. Save the YAML in Kestra's flow editor or import it from the blueprint catalog; extend is catalog metadata, so omit that block if your raw flow validator rejects non-flow fields. Validate the executable flow on your installation.
  3. Keep dry_run=true and auto_remediate=false. Publish healthy and anomalous samples with current UTC timestamps, or supply a manual telemetry input. Inspect score outputs and the dry-run Redis key. No live messages are sent.
  4. Change the input default dry_run to false for unattended live Slack alerts, keeping auto_remediate=false. Live state begins separately from simulation.
  5. After testing device expiry/deduplication and safe-mode behavior, explicitly change the auto_remediate default to true. Observe broker and device receipts.

Outputs and operational limits

  • outputs.validate_telemetry.vars.state_key: Redis key scoped by namespace, flow, mode and device; source is mqtt or manual.
  • outputs.score_health.vars.accepted: false for duplicates/older samples.
  • Accepted samples expose outputs.score_health.vars.health_score, raw_health_score, status (HEALTHY/PENDING/CRITICAL/IDLE), trend, metric_risks, anomaly_count, sustained_anomaly, critical_metrics, alert_due and command_due. command_due describes eligibility, not actual publication.
  • outputs.score_health.vars.state / after_alert_state: health state snapshots; final stored state may also include a command receipt.
  • outputs.prepare_safe_mode.vars.command: command intent, only when authorized; outputs.publish_safe_mode.messagesCount: broker publication result if executed. concurrency.limit=1 serializes this flow's state transactions; allowConcurrent queues MQTT executions. Use one writer for each scoped key and low-rate samples. Queue backlog can make samples stale. This flow does not detect missing telemetry and it does not reorder old samples. For large fleets, partition by asset group with distinct namespaces/flows and subscriber IDs; avoid overlapping key writers. Broker connection/JSON deserialization errors can happen before an execution exists and must be monitored separately from the flow errors handler.

References

See How

New to Kestra?

Use blueprints to kickstart your first workflows.