Schedule icon
Webhook icon
WorkingDirectory icon
Script icon
Docker icon
If icon
SlackIncomingWebhook icon
Create icon
Request icon
Log icon

Ingest PLC Telemetry Only When Every Reading Is Trustworthy

Poll OPC UA and Modbus PLC registers, normalize readings, alarm on bad quality or out of bounds values, and post clean batches to your TSDB.

Categories
Infrastructure

How it works

  1. five_minute_poll (io.kestra.plugin.core.trigger.Schedule, cron */5 * * * *) is the standing net and ships disabled: true, so nothing runs until an operator switches it on. on_demand_poll (io.kestra.plugin.core.trigger.Webhook, keyed) accepts an on-demand poll from an edge gateway or an operator session.
  2. telemetry_workspace (io.kestra.plugin.core.flow.WorkingDirectory) opens one shared filesystem so the snapshot written by the poller is the exact file the normalizer reads.
  3. poll_plc_registers (io.kestra.plugin.scripts.python.Script on the io.kestra.plugin.scripts.runner.docker.Docker runner, image python:3.12-slim) simulates the OPC UA and Modbus poll by capturing eight registers - three temperature, two pressure and three rpm - with node ids, holding register addresses, engineering values, units and OPC UA status names, writes them to opcua-points.json, reads the file back, and emits the raw count and the poll timestamp through the outputs protocol.
  4. normalize_readings (io.kestra.plugin.scripts.python.Script on python:3.12-slim) rewrites every register into the canonical {tag, value, unit, quality, ts} shape, deriving the tag from the OPC UA node id and lowercasing the status name into good, uncertain or bad. A reading is flagged when its quality is not good, its value is not numeric, or it sits outside the engineering bounds for its unit - Celsius up to temperature_limit_c, bar up to 250, rpm up to 3600. An empty or unreadable snapshot is itself flagged, so a dead poll path can never masquerade as a quiet plant.
  5. check_verdict (io.kestra.plugin.core.flow.If) branches on the verdict. On ALARM, alert_plc_deviation posts the flagged count, the peak temperature and the withheld batch to Slack, and open_telemetry_issue files a labeled GitHub issue carrying the per-tag reasons. On CLEAN, post_batch_to_tsdb (io.kestra.plugin.core.http.Request) POSTs the normalized batch to the ingest endpoint taken from the TSDB_INGEST_URL secret, with exponential retries, and log_all_clear records the all-clear in the execution history.
  6. The errors block alerts Slack whenever the poll, the normalization or the POST breaks, because a dead ingest path looks exactly like a clean line.

What you get

  • Gateable results, verdict, points_ingested, bad_quality and max_temp are flow outputs any downstream flow or dashboard can branch on.
  • A canonical batch, every reading lands in the TSDB as {tag, value, unit, quality, ts} so dashboards and alerts never parse raw register shapes again.
  • Escalation where the work happens, a Slack alarm with the flagged count and peak temperature, plus a labeled GitHub issue listing each flagged tag and its reason, while a clean batch simply flows into the TSDB.
  • An ingest audit trail, every execution is a dated record of what was polled, what was flagged and what was shipped.

Who it's for

  • Plant and OT engineers who pull OPC UA and Modbus registers into a time series database and cannot let a bad sensor poison the series.
  • SRE and platform teams running edge gateways who want a deterministic, replayable ingest path with escalation built in.
  • Anyone who has watched a stuck register quietly write nonsense into a dashboard for a week.

Why orchestrate this with Kestra

A raw OPC UA or Modbus client can read registers, but it cannot decide what is trustworthy, escalate what is not, and prove after the fact what was ingested. Kestra supplies the loop around the poll, a disabled-by-default five minute schedule as the safety net, a keyed webhook for on demand runs, a containerized poller and normalizer so the tooling version lives in the flow file, a verdict branch that separates the alarm from the ingest, retry on the TSDB POST, and execution history with the metrics attached. A cron entry on a gateway gives you none of that, it only reads the registers someone remembered to trust.

Prerequisites

  • A worker with Docker access, the poller and the normalizer both run in python:3.12-slim.
  • A TSDB ingest endpoint that accepts a JSON POST with a points array, for example InfluxDB, TimescaleDB or VictoriaMetrics behind a write proxy.
  • A GitHub token that can open issues in github_repository (classic PAT scope repo, or fine-grained Issues: write).
  • A Slack incoming webhook for the deviation alarm and for failure alerts.

Secrets

  • PLC_WEBHOOK_KEY: key segment guarding the on-demand webhook URL, anyone holding it can trigger a poll.
  • TSDB_INGEST_URL: full URL of the time series database ingest endpoint the clean batch is POSTed to.
  • GITHUB_TOKEN: token used to open the deviation issue.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL.

Quick start

  1. Add the four secrets to your Kestra namespace.
  2. Run the flow once with the default inputs and confirm verdict, points_ingested, bad_quality and max_temp in the outputs. The bundled fixture is clean, so the first run POSTs all eight readings.
  3. Flip one fixture quality flag to Uncertain or push a temperature above temperature_limit_c inside poll_plc_registers, then re-run and watch the alarm branch open the issue and skip the TSDB POST.
  4. Trigger on_demand_poll at /api/v1/{tenant}/executions/webhook/{namespace}/opcua-modbus-telemetry-ingest/{PLC_WEBHOOK_KEY}, or enable five_minute_poll once the worker reaches the line gateway.

How to extend

  • Replace the fixture snapshot with a io.kestra.plugin.core.http.Request against the gateway REST endpoint, keeping the fixture as the offline fallback.
  • Add bounds for additional units, for example flow or vibration, by extending the BOUNDS map in normalize_readings.
  • Fan the flagged list out with io.kestra.plugin.core.flow.ForEach so each bad tag opens its own issue or maintenance ticket.
  • Turn a withheld batch into a red run by adding io.kestra.plugin.core.execution.Fail under check_verdict.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.