TL;DR: The sensor data problem isn't that you're not collecting it — Home Assistant's SQLite recorder captures almost everything by default. The problem is that SQLite starts choking on range queries once your history table grows past a few weeks of dense sensor data.
📖 Reading time: ~22 min
What's in this article
- The Problem: Home Automation Data Is Scattered and Unactionable
- Hardware and OS Prerequisites Before You Touch a Config File
- Docker Compose Stack: InfluxDB 2.x, Grafana, and Telegraf
- Pulling Home Automation Data Into the Pipeline
- Building the Grafana Dashboard: Panels That Actually Tell You Something
- Retention, Storage, and Keeping the Pi From Dying Under Load
- Wiring Grafana Alerts Into Your Automation Pipeline
The Problem: Home Automation Data Is Scattered and Unactionable
The sensor data problem isn't that you're not collecting it — Home Assistant's SQLite recorder captures almost everything by default. The problem is that SQLite starts choking on range queries once your history table grows past a few weeks of dense sensor data. Run EXPLAIN QUERY PLAN SELECT * FROM states WHERE entity_id = 'sensor.living_room_temp' AND last_updated > datetime('now', '-30 days') against a mature HA database and you'll see full table scans. The default purge interval is 10 days precisely because the recorder isn't designed for long-term analytical queries — it's designed for state restoration and the last-24-hours view.
The fragmentation problem compounds this. A typical home automation setup ends up with Home Assistant for sensor state, a router admin panel (UniFi, OpenWrt, or whatever shipped with your ISP box) for network device presence and bandwidth, a UPS management interface for power draw, and maybe a separate Uptime Kuma instance for service health. None of these talk to each other. Correlating "my NAS went offline at 2am" with "there was a power fluctuation at 1:58am" requires you to mentally stitch together timestamps from four browser tabs. That's not analysis — that's archaeology.
The gap this stack actually closes is the distance between raw logged values and actionable pattern recognition. Having temperature readings in a log file tells you nothing until you can overlay them against HVAC runtime, time-of-day, and outdoor weather. Having power draw numbers is useless until you can set an alert threshold that fires when the whole-home draw spikes above baseline for more than five minutes. Grafana's alerting engine and InfluxDB's Flux query language are specifically built for exactly these cross-signal correlations — and they run comfortably on modest hardware.
Here's what this article builds, concretely: a Raspberry Pi 4 with 4GB RAM minimum (8GB gives you headroom to add more exporters later), running three containers via Docker Compose — InfluxDB 2.x as the time-series store, Grafana OSS (currently 10.x) as the visualization and alerting layer, and a lightweight Telegraf collector that pulls from Home Assistant's REST API, local system metrics, and SNMP or ping checks for network devices. The Pi handles this workload without breaking a sweat; InfluxDB's TSM storage engine is far more efficient than SQLite for time-series shapes, and Grafana's memory footprint at idle is well under 200MB. The whole stack fits on a 32GB SD card with room to spare, though a USB SSD is strongly recommended for write endurance on any InfluxDB deployment that's ingesting data continuously.
Hardware and OS Prerequisites Before You Touch a Config File
The SD card failure mode is the first thing to get right, and most tutorials skip it. InfluxDB 2.x is write-heavy by design — every scrape interval hammers the storage layer with small random writes, which is exactly what wears out NAND flash on cheap microSD cards. A Class 10 card will die quietly, usually corrupting your database before it throws any obvious errors. Before you commit to any storage layout, boot the Pi, run your planned scrape workload for 20 minutes, and check what's actually hitting the disk:
sudo apt install iotop -y
sudo iotop -ao --only
The -a flag shows accumulated I/O rather than instantaneous rates, and --only filters to processes actively doing I/O. If you see InfluxDB or the kernel's journal flushing multiple megabytes per minute, a microSD is going to lose that fight inside a few months of continuous operation. A USB 3.0-attached SSD — even a cheap SATA SSD in a USB enclosure — changes the write endurance math entirely. On a Pi 4, USB 3.0 is shared bandwidth with the NIC, so don't expect full SATA speeds, but you'll still get dramatically better random write performance than any SD card. On a Pi 5, the situation is better: you can attach an NVMe drive via the PCIe FFC connector with an appropriate HAT.
Raspberry Pi OS Lite (64-bit, Bookworm) is the correct base image — not the desktop variant, not the 32-bit build. InfluxDB 2.x only ships ARM64 binaries; the 32-bit ARMv7 Debian packages don't exist in their official repo. Bookworm (Debian 12) also gives you a kernel new enough to not fight with Docker's network namespace handling. Flash it with Raspberry Pi Imager, use the advanced options to pre-configure your SSH key and hostname before first boot, and never attach a monitor. If you're booting headless from the start, you eliminate an entire category of "works on my desk, breaks in the closet" problems.
Install Docker Engine using the official convenience script, not whatever version is in the Raspberry Pi OS package repos — that copy is usually multiple major versions behind:
# One-liner installs the current stable Docker Engine for your architecture
curl -fsSL https://get.docker.com | sh
# Add your user to the docker group so you're not sudo-ing every command
sudo usermod -aG docker $USER
# Log out and back in, then verify
docker run --rm hello-world
Don't install Docker Desktop — it adds a VM layer that makes zero sense on a Pi and its ARM builds have historically lagged. The Engine-only install is what you want for headless server use.
Assign a static IP through your router's DHCP reservation table using the Pi's MAC address, not by editing /etc/dhcpcd.conf on the Pi itself. The reason is operational: when you need to re-image the Pi — and you will need to, eventually — a static lease on the router survives the wipe. Your Grafana bookmarks, your MQTT broker address, your n8n webhook URLs all stay valid without touching any config. Hardcoding the IP in dhcpcd.conf means you have to remember to replicate that config every time you re-flash. The router-side reservation costs you nothing and makes the Pi functionally disposable, which is exactly the right operational posture for a device running 24/7 in a closet.
Docker Compose Stack: InfluxDB 2.x, Grafana, and Telegraf
The biggest mistake people make with this stack is using latest tags everywhere and wondering why things break after a docker compose pull three months later. Pin InfluxDB specifically — the 2.x config model, Flux query language behavior, and bucket auth scheme changed enough between minor versions that an unpinned update will silently break your Telegraf writes. Use influxdb:2.7 and telegraf:1.30. Grafana OSS moves faster and is generally safer to track at a recent pinned tag, but even there, pin it once your dashboards are stable.
# docker-compose.yml
version: "3.8"
networks:
telemetry:
driver: bridge
volumes:
influxdb-data:
grafana-data:
services:
influxdb:
image: influxdb:2.7
container_name: influxdb
restart: unless-stopped
networks:
- telemetry
ports:
- "8086:8086"
volumes:
- influxdb-data:/var/lib/influxdb2
environment:
- DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_USERNAME=${INFLUXDB_ADMIN_USER}
- DOCKER_INFLUXDB_INIT_PASSWORD=${INFLUXDB_ADMIN_PASSWORD}
- DOCKER_INFLUXDB_INIT_ORG=${INFLUXDB_ORG}
- DOCKER_INFLUXDB_INIT_BUCKET=${INFLUXDB_BUCKET}
# Token is loaded from .env — never hardcode here
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=${INFLUXDB_ADMIN_TOKEN}
grafana:
image: grafana/grafana-oss:11.1.0
container_name: grafana
restart: unless-stopped
networks:
- telemetry
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
depends_on:
- influxdb
telegraf:
image: telegraf:1.30
container_name: telegraf
restart: unless-stopped
networks:
- telemetry
volumes:
# Read-only bind mount — telegraf has no reason to write back to this file
- ./telegraf.conf:/etc/telegraf/telegraf.conf:ro
depends_on:
- influxdb
# Pi-specific: expose host thermal zone to the container
devices:
- /dev/gpiomem:/dev/gpiomem
# .env (chmod 600 this file, add to .gitignore)
INFLUXDB_ADMIN_USER=admin
INFLUXDB_ADMIN_PASSWORD=changeme_strong_password
INFLUXDB_ORG=homelab
INFLUXDB_BUCKET=telemetry
# Generate with: openssl rand -hex 32
INFLUXDB_ADMIN_TOKEN=your_generated_hex_token_here
DOCKER_INFLUXDB_INIT_MODE=setup only runs the bootstrap sequence when the data volume is empty. Once influxdb-data has content, InfluxDB ignores those env vars entirely — so leaving them in the compose file after first boot is harmless. What's not harmless: hardcoding the token in the compose file itself, which ends up in shell history, CI logs, and git repos. Keep it in .env, lock the permissions, and add .env to your .gitignore before the first git add.
# telegraf.conf — minimal but useful for a Pi telemetry node
[agent]
interval = "10s"
round_interval = true
flush_interval = "10s"
hostname = "raspberrypi"
[[outputs.influxdb_v2]]
# Use the service name on the shared bridge — not localhost
urls = ["http://influxdb:8086"]
token = "${INFLUXDB_ADMIN_TOKEN}"
org = "${INFLUXDB_ORG}"
bucket = "${INFLUXDB_BUCKET}"
[[inputs.cpu]]
percpu = true
totalcpu = true
collect_cpu_time = false
[[inputs.mem]]
[[inputs.disk]]
ignore_fs = ["tmpfs", "devtmpfs", "overlay"]
[[inputs.net]]
interfaces = ["eth0", "wlan0"]
[[inputs.temp]]
# On Pi 4/5 this reads from /sys/class/thermal/thermal_zone0/temp
# Reports in Celsius as 'temp' measurement with tag sensor=cpu-thermal
Two things to get right in the Telegraf config that the docs don't emphasize: first, use the Docker service name influxdb as the hostname in urls, not localhost — these containers are on the same bridge network, and localhost inside the Telegraf container points to the Telegraf container itself. Second, pass the token and org as environment variables referencing the same .env values rather than duplicating the literal string. Telegraf 1.30 expands ${VAR} syntax in the config file directly, so you get one source of truth. The inputs.temp plugin on a Pi 4 or Pi 5 reads from /sys/class/thermal/ without any extra kernel modules — you'll see a measurement named temp with a sensor tag value of cpu-thermal, which maps cleanly to a Grafana stat panel.
Named Docker volumes for InfluxDB and Grafana data are non-negotiable if you ever run docker compose down — which you will, during updates. Bind mounts work too, but named volumes survive accidental directory moves and are easier to back up with docker run --rm -v influxdb-data:/data busybox tar czf - /data. The only bind mount in this stack is telegraf.conf mounted read-only, because you want to edit it from the host without exec-ing into the container, and Telegraf has no legitimate reason to modify its own config file at runtime. If it can't write there, a misconfiguration or a bad plugin can't corrupt it either.
Pulling Home Automation Data Into the Pipeline
The least obvious thing about the Home Assistant → InfluxDB path is that it's event-driven, not time-series sampled. That distinction will bite you the first time you build a Grafana graph and see a door sensor drop off the chart for six hours because nothing happened. Keep that in mind for everything that follows.
Home Assistant InfluxDB v2 Integration
The official integration supports InfluxDB v2's API but you have to opt into it explicitly — the default assumes v1. Your configuration.yaml block needs to look like this:
influxdb:
api_version: 2
host: 192.168.1.x # your Pi's LAN IP, not localhost if HA runs elsewhere
port: 8086
ssl: false # skip TLS unless you've got a cert chain sorted
token: your-influxdb-token-here
organization: homelab
bucket: home_automation
include:
entity_globs:
- sensor.living_room_*
- sensor.outdoor_*
- binary_sensor.front_door
exclude:
entity_globs:
- sensor.*_last_updated
- sensor.*_friendly_name
The include/exclude filtering is not optional if you care about bucket hygiene. A default Home Assistant install with a handful of integrations will push thousands of state changes per day — most of them are UI state, internal metadata, or automation flags that have no business in your metrics store. Be specific with entity_globs from day one; retrofitting this after six weeks of accumulated noise is painful. After restarting HA, tail the log and watch for InfluxDB write errors — the most common is a mismatched organization name (case-sensitive).
MQTT Bridge via Telegraf
Zigbee2MQTT, Tasmota, and ESPHome all publish to MQTT natively. Wiring Telegraf directly to Mosquitto means you're capturing that data independently of whether Home Assistant is running — useful during HA restarts or migrations. In your telegraf.conf:
[[inputs.mqtt_consumer]]
servers = ["tcp://192.168.1.x:1883"]
topics = ["home/#", "zigbee2mqtt/#", "tele/+/SENSOR"]
data_format = "json"
json_time_key = "" # let Telegraf timestamp on ingest
qos = 0
connection_timeout = "30s"
persistent_session = false
client_id = "telegraf-home"
The data_format = "json" setting does most of the heavy lifting — Telegraf will flatten nested JSON keys into field names automatically. A Zigbee2MQTT payload like {"temperature": 21.4, "humidity": 58, "linkquality": 103} lands in InfluxDB as three separate fields on one measurement. Tasmota's tele/+/SENSOR payloads are slightly more nested, so you may need json_string_fields or a processor to unwrap them cleanly. The wildcard home/# topic is aggressive — add a tag filter or namepass rule if you're picking up MQTT traffic from non-sensor sources like presence detection or alarm states.
HTTP Scraping for Power Monitors
Shelly devices expose a JSON REST endpoint at http://<device-ip>/status (gen1) or http://<device-ip>/rpc/Switch.GetStatus?id=0 (gen2/Plus). TP-Link Kasa requires a slightly different approach since it uses an encrypted UDP protocol by default, but the python-kasa library can expose a local HTTP shim you can then scrape. Telegraf's inputs.http handles both cleanly:
[[inputs.http]]
urls = [
"http://192.168.1.50/status", # Shelly 1PM
"http://192.168.1.51/status", # Shelly EM
]
method = "GET"
interval = "10s"
data_format = "json"
name_override = "shelly_power"
# Pull only what matters
json_query = "meters"
A 10-second interval is a reasonable floor for energy data on a Pi 4 — you get enough resolution to catch appliance startup spikes without the CPU overhead of sub-second polling across a dozen devices. Shelly gen1 devices have no rate limiting on the local HTTP endpoint so you can go tighter if needed, but energy billing math doesn't benefit from more than 10s granularity. One gotcha: if your Shelly is on a different VLAN from the Pi, IGMP snooping or inter-VLAN routing issues will silently drop the scrape without a useful error in Telegraf — check with curl from the Pi first before debugging the plugin config.
Handling the Gap Problem in Grafana
Because the HA InfluxDB integration only writes on state change, binary sensors — door contacts, motion detectors, window sensors — will appear as single data points with long gaps between them. Grafana interprets these gaps as missing data and draws nothing. The fix is per-panel: under Field overrides, set Graph styles → Fill below to → previous, or in the older panel editor look for Null values: connected and set it to Fill: previous. This tells Grafana to extend the last known value forward until the next data point, which is the semantically correct behavior for a door that's been closed since 9am. Do not apply this setting to numeric sensors like temperature — you want the gap to be visible there, because a missing temperature reading means your sensor is offline, not that the temperature is unchanged.
Building the Grafana Dashboard: Panels That Actually Tell You Something
Most Grafana dashboards I've seen in home-lab write-ups are tourist dashboards — pretty graphs that you look at once and never open again. The ones worth keeping are built around questions you actually ask: is the house using too much power right now?, did the bedroom get hot while I was away?, which sensor went silent three hours ago? Panel layout should answer those in order, top to bottom, without hunting.
Flux Over InfluxQL — Not a Preference, a Capability Gap
Connect Grafana to InfluxDB using the InfluxDB (Flux) datasource, not the legacy InfluxQL one. The config difference is a single radio button in the datasource settings, but the query capability gap is significant. Flux's aggregateWindow() handles time bucketing and gap-filling natively. movingAverage() smooths noisy sensor data without a subquery. A 5-minute average CPU temperature query on my setup looks like this:
from(bucket: "homelab")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "temp" and r.host == "rpi-main")
|> aggregateWindow(every: 5m, fn: mean, createEmpty: false)
|> yield(name: "mean_temp")
createEmpty: false is the flag the docs bury — without it, gaps in sensor data produce null-filled windows that Grafana renders as dropped lines, which looks like sensor failures when it isn't. The yield() at the end is required when you're running multiple queries in the same panel; Grafana uses the yield name as the series label. For power draw with a moving average overlay, chain |> movingAverage(n: 3) after the aggregation window — that smooths over momentary spikes without hiding real sustained load.
Panel Layout Tied to Operator Workflow
The layout I run follows a deliberate information hierarchy. Top row: two single-stat panels — current power draw in watts and current indoor temperature. These use the Stat panel type with thresholds configured (green below 200W, yellow 200–350W, red above). They give you a pass/fail read at a glance without opening a graph. Middle section: two time-series panels spanning 24 hours — one for power, one for temperature across zones. Time range is fixed to now-24h rather than the dashboard-global variable so it stays useful regardless of what someone drags the global range to. Bottom row: a Table panel that queries last-seen timestamps for every MQTT device:
from(bucket: "homelab")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "mqtt_last_seen")
|> last()
|> keep(columns: ["device_id", "_time", "_value"])
|> sort(columns: ["_time"], desc: false)
Sorting ascending by _time puts the longest-silent device at the top — exactly the one you want to see first. Apply a cell color threshold on the _time column: anything older than 30 minutes turns red. That table has caught two dead sensor batteries on my setup before I'd have noticed any alert.
Alerting Without Giving Grafana Cloud Your Email Address
Grafana OSS 9+ has a fully functional alerting engine without any cloud dependency. Set up a contact point under Alerting → Contact points. For local email, configure SMTP in grafana.ini:
[smtp]
enabled = true
host = your-smtp-relay:587
user = alerts@yourdomain.local
password = yourpassword
from_address = grafana@yourdomain.local
skip_verify = false
For anything more than email — routing to a specific n8n flow, posting to a local Matrix room, triggering a Home Assistant webhook — use the Webhook contact point type with your n8n webhook URL. The payload Grafana sends is JSON with the alert state, labels, and values. In n8n, a single Webhook trigger node receives it, and you can branch from there. The alert rule that matters most: on the power draw panel, define an alert that fires when consumption exceeds your threshold for 5 consecutive minutes. That "for" duration is set in the alert rule's Pending period field — set it to 5m. Without a pending period, a kettle or microwave kicks off a false alert every morning.
Dashboard JSON in Version Control, Not Just in the Database
Grafana stores dashboards in its internal SQLite or Postgres database, which means a container rebuild or volume wipe takes them with it. The fix is provisioning: export the dashboard JSON from Dashboard settings → JSON model, commit it to a git repo, then mount it into the container and tell Grafana where to look. In your Docker Compose file:
services:
grafana:
image: grafana/grafana-oss:10.4.2
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
- ./grafana/dashboards:/var/lib/grafana/dashboards
environment:
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
The provisioning directory needs a dashboard provider config at ./grafana/provisioning/dashboards/homelab.yaml:
apiVersion: 1
providers:
- name: homelab
folder: Home Automation
type: file
disableDeletion: true
updateIntervalSeconds: 30
options:
path: /var/lib/grafana/dashboards
disableDeletion: true prevents someone from accidentally deleting the provisioned dashboard from the UI — it'll just reappear on the next sync interval. The updateIntervalSeconds: 30 means a git pull followed by copying the updated JSON into the dashboards directory takes effect in under a minute without a container restart. That's the loop: edit in Grafana UI, export JSON, commit, done. Container rebuilds are now safe.
Retention, Storage, and Keeping the Pi From Dying Under Load
The most common Pi telemetry setup failure I see documented online skips the storage math entirely and then wonders why the SD card is dead or the disk is full in three weeks. Before you commit to any hardware, run your intended Telegraf config for 24 hours and pull actual write rates from InfluxDB's built-in /metrics endpoint — it exposes Prometheus-format output at http://localhost:8086/metrics and includes influxdb_write_points_ok_total so you can calculate your actual ingestion rate rather than guessing. Pair that with docker stats watching the InfluxDB container and you'll know whether a 64GB SSD is sufficient before you're committed. For a typical home setup — a handful of temperature/humidity sensors, one or two power monitors, and basic system metrics — 30 days of retention on a 64GB SSD is about the practical ceiling before you start chasing disk space.
Set your bucket retention policy at init time, not after the fact. If you're running InfluxDB 2.x in Docker, pass it during the first boot:
docker run -d \
--name influxdb \
-p 8086:8086 \
-v influxdb-data:/var/lib/influxdb2 \
-e DOCKER_INFLUXDB_INIT_MODE=setup \
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
-e DOCKER_INFLUXDB_INIT_PASSWORD=yourpassword \
-e DOCKER_INFLUXDB_INIT_ORG=homelab \
-e DOCKER_INFLUXDB_INIT_BUCKET=telemetry \
-e DOCKER_INFLUXDB_INIT_RETENTION=720h \ # 30 days
influxdb:2.7
Changing retention after the fact via the UI works fine, but the old data doesn't retroactively shrink — InfluxDB runs shard compaction on its own schedule. If you overshoot your retention and need to reclaim space immediately, you'll need to drop and recreate the bucket, which means losing history. Setting it correctly upfront is the only safe path.
Telegraf input intervals are where cardinality problems quietly compound. The safe defaults: system-level metrics (CPU, memory, disk I/O) at interval = "10s", HTTP scrapes of external APIs or device endpoints at interval = "30s", and MQTT inputs left event-driven — meaning no polling interval, just [[inputs.mqtt_consumer]] subscribing and writing on receipt. The mistake is setting a global interval and letting MQTT inherit it. If a sensor fires 20 events per minute and you're also polling it on a 10-second schedule, you're doubling your write volume and creating duplicate tags that inflate cardinality. InfluxDB's cardinality limit isn't documented as a hard wall in the OSS tier, but series cardinality above roughly 10–15 million on a constrained device will cause measurable query slowdown.
On a Pi running from SD card, InfluxDB's WAL (write-ahead log) will kill your card faster than anything else in this stack. The WAL flushes frequently by design — it's what makes InfluxDB durable. After the first 48 hours of operation, run:
sudo dmesg | grep -i error
Any mmc0 I/O errors or EXT4-fs error lines at this stage mean your SD card is already degrading under the write load. On an SSD via USB 3.0 or the Pi 5's PCIe M.2 slot this doesn't happen — SSDs handle random write patterns from WAL workloads without issue. If you're committed to SD card for some reason, at minimum move the InfluxDB data directory to a USB drive and keep the OS on the card. Don't run the WAL on SD and expect it to last a year.
For backups, a nightly cron job to an NFS share or external USB drive is straightforward and the restore path is actually reliable — unlike some tools that produce backup archives you can't test without a full cluster. The InfluxDB CLI backup format is portable across 2.x minor versions:
# /etc/cron.d/influx-backup
0 2 * * * root influx backup /mnt/backup/influx/$(date +\%Y-\%m-\%d) \
--host http://localhost:8086 \
--token $INFLUX_TOKEN
Store INFLUX_TOKEN in a root-only readable file and source it in the cron environment, or pass it via a wrapper script — cron's environment doesn't inherit your shell exports. To verify the backup is actually restorable, spin up a temporary InfluxDB container, run influx restore against it, and confirm your bucket and series show up. Do this once. Backups you've never tested restored aren't backups.
Wiring Grafana Alerts Into Your Automation Pipeline
The most underrated part of a Raspberry Pi telemetry setup isn't the data collection — it's closing the feedback loop so the dashboard actually does something. Grafana's webhook contact point paired with an n8n trigger is how you get from "I can see the power draw spiking" to "the switch turned off automatically and I got a push notification." The wiring is straightforward once you know the exact shape of the payload.
In Grafana, go to Alerting → Contact points → New contact point, select Webhook, and paste in your n8n webhook URL. On my setup that looks like http://192.168.1.x:5678/webhook/grafana-alerts — internal IP, no auth header required since it's LAN-only, though you can add a Basic Auth header if your n8n instance is exposed. The payload Grafana sends looks roughly like this:
{
"receiver": "n8n-webhook",
"status": "firing",
"alerts": [
{
"status": "firing",
"labels": {
"alertname": "HighPowerDraw",
"device": "washing_machine_plug"
},
"values": {
"watts": 2340
},
"state": "Alerting"
}
]
}
Inside n8n, the Webhook trigger node receives that body and you route on {{ $json.alerts[0].status }} and {{ $json.alerts[0].labels.device }}. Drop an IF node after it: if status is firing and device matches the plug you care about, continue down the action branch. The HTTP Request node then calls Home Assistant's REST API:
POST http://homeassistant.local:8123/api/services/switch/turn_off
Headers:
Authorization: Bearer YOUR_HA_LONG_LIVED_TOKEN
Content-Type: application/json
Body:
{
"entity_id": "switch.washing_machine_plug"
}
Chain a Pushover node after that (n8n has a native Pushover node — credentials take 30 seconds to configure) and template the message with {{ $json.alerts[0].values.watts }}W detected on {{ $json.alerts[0].labels.device }} — switch turned off. The whole flow from Grafana firing to your phone buzzing runs in under a minute of wall time once it's built, and maybe 10 minutes to actually build it. For a broader look at how webhook-driven pipelines like this fit into a self-hosted automation stack, see our guide on Workflow Automation in 2026: n8n, Zapier, and Self-Hosted Pipelines.
The gotcha that wastes real time: on Grafana versions below 10.x, the legacy alerting engine either doesn't send a resolved webhook at all, or sends it in a different schema than the firing payload. That means your n8n flow never gets the "all clear" signal, and if your logic depends on toggling state back — like re-enabling a switch once the draw drops — it silently breaks. Upgrade to Grafana 10.x or later; the unified alerting engine sends both firing and resolved states consistently, and the alerts[0].status field is reliable in both directions. If you're pinned to an older version for some reason, the workaround is polling Home Assistant's sensor state from n8n on a schedule and checking it independently — ugly, but functional.
Disclaimer: This article is for informational purposes only. The views and opinions expressed are those of the author(s) and do not necessarily reflect the official policy or position of Sonic Rocket or its affiliates. Always consult with a certified professional before making any financial or technical decisions based on this content.
Originally published on techdigestor.com. Follow for more developer-focused tooling reviews and productivity guides.
Top comments (1)
Nice write-up! For devs who deal with messy copied text, TextStow might help — it's a Mac menu bar tool combining clipboard history with prompt templates and text cleanup. Free: textstow.com