background-shape
Real-Time Dashboards for IIoT with Grafana 9
August 22, 2022 · 4 min read · by Muhammad Amal programming

TL;DR — Grafana 9 + TimescaleDB SQL data source = solid IIoT dashboards. Use time_bucket for downsampling on the fly. For real-time streaming, add the MQTT data source plugin. Build per-line, per-machine views; embed in shop-floor screens via kiosk mode.

After ingesting MQTT into TimescaleDB, the visualization layer. Grafana 9 (released June 2022) is the de facto OSS dashboard.

Connecting TimescaleDB

In Grafana → Data Sources → PostgreSQL (TimescaleDB shows up under PostgreSQL):

Host:     timescaledb:5432
Database: sensor_data
User:     grafana_ro
Password: ****
SSL Mode: disable (internal) or require (across networks)

Use a read-only DB user. Grafana queries; never writes.

Test connection. Once green, panels can use it.

A first panel — time series

Panel type: Time series. Query (TimescaleDB SQL):

SELECT
  $__timeGroupAlias(ts, $__interval),
  metric AS "Metric",
  avg(value) AS "Avg"
FROM sensor_readings
WHERE device_id = 'press-42'
  AND metric IN ('temperature', 'pressure')
  AND $__timeFilter(ts)
GROUP BY 1, metric
ORDER BY 1

Grafana macros:

  • $__timeGroupAlias(ts, $__interval) — bucket by Grafana’s chosen interval based on zoom level
  • $__interval — interval Grafana picked
  • $__timeFilter(ts) — restricts to dashboard’s time range

For data older than the visible range, Grafana doesn’t load it. For high-frequency data (10 Hz+), use a continuous aggregate so the query is fast:

SELECT
  bucket AS time,
  metric,
  avg_value
FROM sensor_5min
WHERE device_id = 'press-42'
  AND $__timeFilter(bucket)
ORDER BY bucket

Hits the materialized view instead of raw data. Faster.

Variable templates

For a single dashboard covering many devices:

In dashboard settings → Variables → Add:

Name:  device
Type:  Query
Query: SELECT DISTINCT device_id FROM sensor_readings ORDER BY device_id

A dropdown appears at the dashboard top. Queries reference $device:

SELECT ...
FROM sensor_readings
WHERE device_id = '$device'

Now one dashboard serves N devices.

Stat panel for current values

SELECT
  value
FROM sensor_readings
WHERE device_id = '$device' AND metric = 'temperature'
ORDER BY ts DESC
LIMIT 1

Display as Stat panel. Shows the latest reading. Big number, color-coded by threshold.

Configure thresholds:

Green:  -∞ to 70
Yellow: 70 to 85
Red:    85+

Now a glance tells you “machine is over-temp.”

Gauge for level readings

For pressure, fill level, RPM as percentage:

Panel type: Gauge. Query for latest value. Min/max as panel settings. Useful for shop-floor displays where the operator wants a quick visual.

Tables for status lists

A status overview of all devices:

SELECT
  device_id AS "Device",
  MAX(CASE WHEN metric = 'temperature' THEN value END) AS "Temp",
  MAX(CASE WHEN metric = 'pressure' THEN value END) AS "Pressure",
  MAX(ts) AS "Last Seen"
FROM sensor_readings
WHERE ts > now() - interval '5 minutes'
GROUP BY device_id
ORDER BY device_id

Returns one row per device. Color cells by threshold. Quick “everything OK?” view.

Real-time streaming via MQTT

Grafana 9 has an MQTT data source plugin (grafana-mqtt-datasource). Configure with the broker URL + auth. Then panels subscribe to topics directly.

For factory shop-floor displays where “last 30 seconds” matters more than historical data, streaming from MQTT skips the DB round-trip.

Note: Grafana’s streaming model is best for sparse updates. For 100 Hz raw data, downsample upstream first.

Dashboard structure for factories

For a typical factory:

  • Overview dashboard — fleet-wide. All machines at a glance. Color-coded health.
  • Line dashboard — per production line. Each machine’s key metrics.
  • Machine dashboard — drill-down. Full history, all metrics, recent events.

Use Grafana’s “link” feature so panels on overview link to line dashboards filtered to the relevant line.

Shop-floor displays

For wall-mounted monitors on the factory floor:

  • Kiosk mode: https://grafana.../d/.../uid?kiosk — full-screen, no UI chrome
  • Playlist: rotate through 3-4 dashboards every 30 seconds
  • Refresh: set dashboard auto-refresh to 5-10 seconds
  • Theme: dark for displays on factory floor (less glare)

Browsers running these displays should be unattended. Set up auto-login user with view-only role.

Annotations for events

Mark events on the timeline (machine started, stopped, fault):

In dashboard settings → Annotations → Add:

SELECT
  ts AS time,
  event_type AS title,
  event_message AS text
FROM machine_events
WHERE device_id = '$device'
  AND $__timeFilter(ts)

Now panels show vertical lines at event times. Useful for correlating “we lost pressure right after the alarm fired.”

Variables I always add

  • $__interval — auto-set; use in time_bucket
  • $device — query-based from devices table
  • $line — query-based, filters devices by line
  • $timeRange — usually Grafana handles via the picker

For larger setups also:

  • $site (multi-factory)
  • $shift (compare day shift vs night shift performance)

Common Pitfalls

Query without $__timeFilter. Loads all data. Slow. Always filter to dashboard time range.

Raw data when an aggregate would do. Past 100K rows per query, the chart loads slowly. Use continuous aggregates.

Too many panels per dashboard. Past ~20, load time degrades. Split into multiple dashboards with cross-links.

Hard-coded device IDs. Use variables. One dashboard serves all devices.

Skipping thresholds and color coding. A line chart with no thresholds is just lines. Color = quick status.

Stale data displayed as current. If MQTT goes down, the dashboard happily shows yesterday’s last reading. Add “data freshness” indicator (alert if now() - max(ts) > 1 minute).

Wrapping Up

Grafana 9 + TimescaleDB + a few well-chosen panels = factory-grade dashboards. Wednesday: anomaly alerting — when to wake someone up.