Real-Time Dashboards for IIoT with Grafana 9
TL;DR — Grafana 9 + TimescaleDB SQL data source = solid IIoT dashboards. Use
time_bucketfor 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.