Grafana for IoT Dashboard and Visualization

Grafana is the leading open-source platform for building IoT monitoring dashboards, capable of visualizing sensor data from InfluxDB, TimescaleDB, PostgreSQL, and other time-series backends with real-time graphs, alerting, and public dashboard sharing. This guide covers building IoT monitoring dashboards, configuring data sources, creating sensor visualizations, setting up alerts, and sharing dashboards.

Prerequisites

  • Ubuntu 20.04/22.04 or CentOS 8/Rocky Linux 8+
  • Root or sudo access
  • InfluxDB 2.x or TimescaleDB/PostgreSQL with IoT data
  • MQTT broker (Mosquitto) collecting sensor data
  • Telegraf for data collection (optional)

Install Grafana

Ubuntu/Debian:

# Add Grafana repository
sudo apt install -y software-properties-common wget
wget -q -O /usr/share/keyrings/grafana.key https://apt.grafana.com/gpg.key

echo "deb [signed-by=/usr/share/keyrings/grafana.key] https://apt.grafana.com stable main" \
  | sudo tee /etc/apt/sources.list.d/grafana.list

sudo apt update
sudo apt install -y grafana

sudo systemctl enable grafana-server
sudo systemctl start grafana-server

CentOS/Rocky Linux:

cat > /etc/yum.repos.d/grafana.repo << 'EOF'
[grafana]
name=grafana
baseurl=https://rpm.grafana.com
repo_gpgcheck=1
enabled=1
gpgcheck=1
gpgkey=https://rpm.grafana.com/gpg.key
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
EOF

sudo dnf install -y grafana
sudo systemctl enable --now grafana-server

Access Grafana at http://your-server:3000. Default credentials: admin / admin. Change the password immediately on first login.

Configure InfluxDB Data Source

# Via Grafana UI:
# Configuration > Data Sources > Add data source > InfluxDB

# For InfluxDB 2.x with Flux query language:
# Name: InfluxDB-IoT
# Query Language: Flux
# URL: http://localhost:8086
# Organization: myorg
# Token: your-influxdb-token
# Default Bucket: iot-sensors

# Test the connection, then save

Sample Flux queries for IoT panels:

// Current temperature per sensor
from(bucket: "iot-sensors")
  |> range(start: -5m)
  |> filter(fn: (r) => r._measurement == "temperature")
  |> filter(fn: (r) => r._field == "value")
  |> last()

// Temperature over time for all devices (time series panel)
from(bucket: "iot-sensors")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "temperature")
  |> filter(fn: (r) => r._field == "value")
  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
  |> yield(name: "mean_temp")

// Filter by variable (device_id from Grafana template variable)
from(bucket: "iot-sensors")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "temperature")
  |> filter(fn: (r) => r.device_id == "${device_id}")
  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)

Configure TimescaleDB Data Source

For IoT data stored in TimescaleDB (PostgreSQL extension):

# Via Grafana UI:
# Configuration > Data Sources > Add data source > PostgreSQL
# Host: localhost:5432
# Database: iotdb
# User: grafana_reader
# Password: readonlypassword
# SSL Mode: disable (or require for production)
# TimescaleDB: ON (enables time_bucket functions)
-- Create a read-only user for Grafana
CREATE USER grafana_reader WITH PASSWORD 'readonlypassword';
GRANT CONNECT ON DATABASE iotdb TO grafana_reader;
GRANT USAGE ON SCHEMA public TO grafana_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO grafana_reader;

Sample TimescaleDB queries:

-- Time series panel: average temperature per 5-minute bucket
SELECT
    time_bucket('5 minutes', time) AS time,
    device_id,
    avg(value) AS avg_temp
FROM sensor_readings
WHERE
    $__timeFilter(time)
    AND measurement = 'temperature'
    AND device_id IN ($device_id)
GROUP BY 1, device_id
ORDER BY 1;

-- Stat panel: latest reading
SELECT value
FROM sensor_readings
WHERE measurement = 'temperature' AND device_id = '${device_id}'
ORDER BY time DESC LIMIT 1;

Build Sensor Dashboards

Create a comprehensive IoT sensor dashboard:

# Dashboard JSON (importable via Grafana UI > Import)
# Use this as a starting point, then customize in the UI

# Via UI: Dashboards > New Dashboard > Add Panel
# Panel types for IoT:
# - Time Series: temperature/humidity over time
# - Gauge: current value with min/max
# - Stat: single current value
# - Geomap: device locations
# - Table: all devices with latest values
# - Alert List: active sensor alerts

Recommended panel configurations:

Temperature Gauge:

  • Panel type: Gauge
  • Title: Room Temperature
  • Unit: Celsius or Fahrenheit
  • Min: 0, Max: 50
  • Thresholds: Green (0-25), Yellow (25-35), Red (35+)

Humidity Time Series:

  • Panel type: Time series
  • Title: Humidity Last 24 Hours
  • Unit: Percent (0-100)
  • Fill opacity: 20
  • Line width: 2

Battery Status Table:

// InfluxDB: Battery status for all devices
from(bucket: "iot-sensors")
  |> range(start: -1h)
  |> filter(fn: (r) => r._measurement == "battery")
  |> filter(fn: (r) => r._field == "percent")
  |> group(columns: ["device_id"])
  |> last()
  |> map(fn: (r) => ({
    r with
    status: if r._value < 20 then "critical"
            else if r._value < 50 then "low"
            else "ok"
  }))

Variable Templates

Template variables make dashboards reusable across devices and locations:

# In Dashboard Settings > Variables > New Variable

# Variable 1: device_id
# Name: device_id
# Label: Device
# Type: Query
# Data source: InfluxDB-IoT
# Query (Flux):
import "influxdata/influxdb/schema"
schema.tagValues(bucket: "iot-sensors", tag: "device_id")

# Variable 2: location  
# Name: location
# Label: Location
# Type: Query (or Custom for static values)
# Custom values: living_room,bedroom,kitchen,garage

# Variable 3: interval (for time aggregation)
# Name: interval
# Label: Interval
# Type: Interval
# Values: 1m,5m,15m,1h,6h,1d

Use variables in panel queries:

// Use ${device_id} and ${interval} variables
from(bucket: "iot-sensors")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r.device_id == "${device_id}")
  |> filter(fn: (r) => r.location == "${location}")
  |> aggregateWindow(every: ${interval}, fn: mean, createEmpty: false)

Alerting Configuration

Set up alerts for sensor threshold violations:

# Via UI: Alerting > Alert rules > New alert rule

# Alert: High Temperature
# Name: High Room Temperature
# Rule type: Grafana managed alert

# Query A (condition):
# from(bucket: "iot-sensors")
#   |> range(start: -5m)
#   |> filter(fn: (r) => r._measurement == "temperature")
#   |> mean()

# Condition:
# WHEN: last() IS ABOVE 30 (degrees)
# For: 5m (must be above threshold for 5 minutes)

# Create notification contact point first:
# Alerting > Contact points > New contact point

Configure a Slack notification contact point:

# Alerting > Contact points > Add contact point
# Type: Slack
# Webhook URL: https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
# Message template:
# {{ define "slack.myorg.text" }}
# {{ range .Alerts }}
# *Alert:* {{ .Labels.alertname }}
# *Device:* {{ .Labels.device_id }}
# *Value:* {{ .Values.A | printf "%.1f" }}°C
# *Status:* {{ .Status }}
# {{ end }}
# {{ end }}

Email notification:

# Configure SMTP in grafana.ini
sudo nano /etc/grafana/grafana.ini

# [smtp]
# enabled = true
# host = smtp.sendgrid.net:587
# user = apikey
# password = your-sendgrid-api-key
# from_address = [email protected]
# from_name = Grafana IoT Alerts
# skip_verify = false

sudo systemctl restart grafana-server

# Add Email contact point via UI:
# Alerting > Contact points > Type: Email
# Addresses: [email protected],[email protected]

Real-Time Data Streaming

Configure Grafana for real-time streaming data:

# Enable live streaming in grafana.ini
sudo nano /etc/grafana/grafana.ini
# [live]
# max_connections = 100

# Use Grafana Live for real-time updates
# In panel query settings: set "Min interval" to match your data frequency

# For real-time MQTT data, use Grafana's WebSocket plugin or
# stream data via InfluxDB's built-in streaming:

# Configure Telegraf to write MQTT data to InfluxDB at 1-second intervals
cat > /etc/telegraf/telegraf.d/mqtt.conf << 'EOF'
[[inputs.mqtt_consumer]]
  servers = ["tcp://localhost:1883"]
  topics = ["home/+/+"]
  username = "telegraf"
  password = "telegrafpassword"
  data_format = "json"
  json_name_key = "measurement"

[[outputs.influxdb_v2]]
  urls = ["http://localhost:8086"]
  token = "your-influx-token"
  organization = "myorg"
  bucket = "iot-sensors"
EOF

sudo systemctl restart telegraf

Public Dashboard Sharing

Share dashboards publicly without requiring Grafana login:

# Enable public dashboards in grafana.ini
sudo nano /etc/grafana/grafana.ini

# [feature_toggles]
# publicDashboards = true

sudo systemctl restart grafana-server

Via UI:

  1. Open a dashboard
  2. Click the Share icon (top toolbar)
  3. Click Public dashboard tab
  4. Toggle Enable public access
  5. Copy the public URL

For embedding in a website:

<!-- Embed a specific Grafana panel -->
<iframe
  src="https://grafana.example.com/d-solo/DASHBOARD_UID/iot?orgId=1&panelId=2&from=now-24h&to=now&theme=light"
  width="800"
  height="400"
  frameborder="0">
</iframe>

<!-- For public dashboards (no auth required) -->
<iframe
  src="https://grafana.example.com/public-dashboards/PUBLIC_DASHBOARD_UID"
  width="100%"
  height="600"
  frameborder="0">
</iframe>

Troubleshooting

Data source connection error:

# Test InfluxDB connectivity from Grafana server
curl -X POST "http://localhost:8086/api/v2/query?org=myorg" \
  -H "Authorization: Token your-token" \
  -H "Content-Type: application/vnd.flux" \
  --data 'from(bucket:"iot-sensors") |> range(start:-5m) |> last()'

# For PostgreSQL/TimescaleDB:
psql -h localhost -U grafana_reader -d iotdb -c "SELECT 1;"

Panels show "No data":

# Check time range matches when data was inserted
# Extend time range to "Last 24 hours" or "Last 7 days"

# Verify measurement names in Flux query
influx query --org myorg 'import "influxdata/influxdb/schema"
schema.measurements(bucket: "iot-sensors")'

# Check for time zone mismatches
# In Grafana: Dashboard Settings > Time zones > Set to server timezone

Alerts not firing:

# Check alert evaluation logs
sudo journalctl -u grafana-server -n 50 | grep -i alert

# Test notification contact point
# Alerting > Contact points > Test button

# Verify alert is in "Normal" state first
# Alerting > Alert rules > Check rule status

Conclusion

Grafana transforms raw IoT sensor data into actionable dashboards with minimal configuration. Pair it with InfluxDB or TimescaleDB for efficient time-series storage, configure template variables to make dashboards reusable across devices and locations, and set up alerts to be notified immediately when sensors exceed thresholds. Public dashboard sharing lets you expose read-only views to stakeholders without requiring Grafana accounts.