Grafana Analytics Setup¶
InfluxDB version: 1.x (Chronograf UI)
Step 1 — InfluxDB: Create Database & User ✅¶
In Chronograf (InfluxDB Web UI):
- Click crown icon (Admin) → InfluxDB tab → Create Database
- Name:
homeassistant - Click Users tab → Create User
- Username:
homeassistant - Grant access to
homeassistantdatabase
Step 2 — Wire HA to InfluxDB¶
In secrets.yaml (HA config root), add:
In configuration.yaml (HA config root), add:
Copy influxdb_ha.yaml from /homeassistant/influxdb_ha.yaml into /config/influxdb_ha.yaml (accessible via Studio Code Server add-on or Samba share).
Restart Home Assistant → Settings → System → Restart
Wait ~5 minutes for data to start flowing into InfluxDB.
Verify data is arriving: In Chronograf → Data Explorer → select homeassistant database → you should see measurements appearing.
Step 3 — Connect Grafana to InfluxDB¶
- Open Grafana (HA → Add-ons → Grafana → Open Web UI)
- Go to Connections → Data Sources → Add new data source
- Select InfluxDB
- Set:
- Query Language: InfluxQL (NOT Flux — that's for v2)
- URL:
http://localhost:8086 - Database:
homeassistant - User:
homeassistant - Password: (your InfluxDB password)
- Click Save & Test — should show green ✓
Step 4 — Build the Dashboard¶
- Grafana → Dashboards → New Dashboard → Add visualization
- Select the InfluxDB data source
- Use InfluxQL queries (see below — different from the Flux queries in grafana_queries.md)
InfluxQL Queries for each panel:¶
Temperature — split into three panels (Time series). The old single "Temperature by Zone" panel dumped every °F sensor onto one chart and was unreadable. It is now three focused panels, each filtered by an entity_id regex and grouped by friendly_name:
-- Comfort — Bedrooms & Office (master bedroom, bedrooms 1–4, office)
SELECT mean("value") FROM "°F"
WHERE $timeFilter AND "entity_id" =~ /^(bedroom(_[1-4])?|office)_temperature$/
GROUP BY time($__interval), "friendly_name" fill(none)
-- Garage Food Temperature (fridge / fridge-freezer / standalone freezer)
SELECT mean("value") FROM "°F"
WHERE $timeFilter AND "entity_id" =~ /^(refrigerator|freezer)_.*temperature$/
GROUP BY time($__interval), "friendly_name" fill(none)
-- Plant Temperature (Fiddle Leaf 1 & 2, Maury River soil sensor)
SELECT mean("value") FROM "°F"
WHERE $timeFilter AND "entity_id" =~ /^(fiddle_leaf_[12]|soil_sensor_1)_temperature$/
GROUP BY time($__interval), "friendly_name" fill(none)
Fiddle Leaf Soil Moisture — trailing 7 days (Time series). Plots the three Fiddle Leaf Fig soil-moisture sensors over a trailing 8-day window. The panel sets a per-panel relative time of 8d (timeFrom) so it ignores the dashboard's 24h range — moisture changes slowly, so a week of context makes watering events legible. Use the _moisture entities (not the duplicate _soil_moisture ones, which report identical values):
SELECT mean("value") FROM "%"
WHERE $timeFilter AND "entity_id" =~ /^(fiddle_leaf_[12]|soil_sensor_1)_moisture$/
GROUP BY time($__interval), "friendly_name" fill(none)
CO₂ (Time series)
SELECT mean("value") FROM "ppm"
WHERE "entity_id" = 'sensor.bedroom_carbon_dioxide'
AND $timeFilter
GROUP BY time($__interval) fill(none)
VOCs (Time series)
SELECT mean("value") FROM "ppb"
WHERE "entity_id" = 'sensor.bedroom_volatile_organic_compounds'
AND $timeFilter
GROUP BY time($__interval) fill(none)
Humidity (Time series)
SELECT mean("value") FROM "%"
WHERE "entity_id" = 'sensor.bedroom_humidity'
AND $timeFilter
GROUP BY time($__interval) fill(none)
Live Power Draw per Plug (Time series)
SELECT mean("value") FROM "W"
WHERE ("entity_id" =~ /front_room_plug_1_power|upstairs_hallway_1_power|upstairs_hallway_plug_2_power|office_plug_1_power/)
AND $timeFilter
GROUP BY time($__interval), "entity_id" fill(none)
Internet Speed — Download / Upload (Time series, one panel each)
-- Download panel
SELECT mean("value") FROM "Mbit/s"
WHERE "entity_id" = 'speedtest_download'
AND $timeFilter
GROUP BY time($__interval) fill(none)
-- Upload panel (same, swap the entity_id)
SELECT mean("value") FROM "Mbit/s"
WHERE "entity_id" = 'speedtest_upload'
AND $timeFilter
GROUP BY time($__interval) fill(none)
Internet Speed — Ping (Time series)
SELECT mean("value") FROM "ms"
WHERE "entity_id" = 'speedtest_ping'
AND $timeFilter
GROUP BY time($__interval) fill(none)
Mbits, Ping uses ms). Speedtest points are sparse, so the panels set spanNulls: true to keep the line continuous. Note the InfluxDB entity_id tag has no sensor. prefix. Garage Door Events (State timeline)
SELECT "value" FROM "state"
WHERE ("entity_id" =~ /left_garage_door|right_garage_door/)
AND $timeFilter
Presence — Who's Home (State timeline)
Leak Sensors (State timeline)
SELECT "value" FROM "state"
WHERE ("entity_id" =~ /leak_detector|leak_sensor|butlers_kitchen_sink|second_floor_hall/)
AND $timeFilter
⚡ Energy (top-5) & Device Health¶
The ⚡ Energy row is the top-5 energy panels — consolidated 2026-06-13 from the old Energy / Energy Intelligence / Circuit Breakdown rows (dropped the unfiltered "Energy Consumption (kWh)" and redundant "TOU Rate History"). 🔋 Device Health follows.
1. Whole-Home Power vs TOU Rate (dual-axis timeseries) — draw on the left, price on the right:
SELECT mean("value") FROM "W" WHERE "entity_id" = 'whole_home_power' AND $timeFilter GROUP BY time($__interval) fill(none)
-- right axis: SELECT mean("value") FROM "$/kWh" WHERE "entity_id" = 'tou_rate' AND $timeFilter GROUP BY time($__interval) fill(previous)
2. Today's Cost (stat) — sensor.energy_cost_today; measurement is literally $ (the sensor's unit):
3. Energy by TOU Period — Today (bar gauge) — today's kWh per tariff (whole_home_energy utility_meter):
SELECT max("value") FROM "kWh" WHERE "entity_id" =~ /^whole_home_energy_(super_off_peak|off_peak|peak)$/ AND $timeFilter GROUP BY "friendly_name" fill(none)
4. Circuit Power — Now (sorted bar gauge) — live per branch circuit (240V appliances summed; mains + raw legs excluded; reduce→sortBy for descending):
SELECT last("value") FROM "W" WHERE "entity_id" =~ /^em_/ AND "entity_id" =~ /_power$/ AND "entity_id" !~ /_l[12]_power$/ AND $timeFilter GROUP BY "friendly_name" fill(none)
-- + 2nd target: FROM "W" WHERE "entity_id" =~ /^(air_handler|stove|dryer)_power$/ …
5. HVAC Power — Condensers + Air Handler (timeseries) — the dominant loads:
SELECT mean("value") FROM "W" WHERE "entity_id" =~ /^(em_condenser_floor[123]|air_handler)_power$/ AND $timeFilter GROUP BY time($__interval), "friendly_name" fill(none)
Renaming an entity churns the InfluxDB
friendly_nametag —friendly_name-grouped panels may briefly show stale duplicate series (e.g. old "AC Condenser 1") until that old-tagged data ages out of the window.
6. HVAC Runtime vs Outdoor High (dual-axis timeseries) — WS2 health check: does runtime track outdoor temperature? Left axis is daily condenser runtime (hours, bars) for all three floors; right axis is the daily outdoor high (°F, line):
-- left axis, one target per floor:
SELECT max("value") FROM "h" WHERE "entity_id" = 'first_floor_condenser_runtime_today' AND $timeFilter GROUP BY time(1d) fill(none)
SELECT max("value") FROM "h" WHERE "entity_id" = 'second_floor_condenser_runtime_today' AND $timeFilter GROUP BY time(1d) fill(none)
SELECT max("value") FROM "h" WHERE "entity_id" = 'third_floor_condenser_runtime_today' AND $timeFilter GROUP BY time(1d) fill(none)
-- right axis:
SELECT max("temperature") FROM "state" WHERE "entity_id" = 'forecast_home' AND $timeFilter GROUP BY time(1d) fill(none)
fahrenheit unit, fixed orange color) while the three floor series stay stacked bars on the left (h unit). This is a visual at-a-glance check only — the quantitative correlation (Pearson r + OLS slope per condenser) is computed by scripts/analysis/hvac_runtime_correlation.py and the results are recorded in projects/house_energy_strategy.md (Workstream 2 findings). 7. Runtime Today — Floor 1 / 2 / 3 (stat, one card per floor) — today's/most-recent daily runtime as the big number, with an area sparkline of daily runtime history over the dashboard time range:
SELECT max("value") FROM "h" WHERE "entity_id" = 'first_floor_condenser_runtime_today' AND $timeFilter GROUP BY time(1d) fill(none)
-- swap the entity_id for second_floor_condenser_runtime_today / third_floor_condenser_runtime_today
reduceOptions.calcs: ["lastNotNull"] picks the most recent day for the big number, while graphMode: area draws the sparkline across all the daily points returned. Device Battery Levels (bar gauge) — every device battery, sorted, red < 15%. The _battery$ regex matches ZHA/camera batteries and excludes phones/tablets (those end in _battery_level):
SELECT last("value") FROM "%"
WHERE "entity_id" =~ /_battery$/ AND $timeFilter
GROUP BY "friendly_name" fill(none)
Zigbee Signal — LQI (bar gauge) — ZHA link quality (0–255) for every Zigbee device; red < 80 (poor), green ≥ 180 (solid). Read it next to the battery panel: a leak sensor with low LQI and fast battery drain is fighting a weak mesh link (lots of retries) — fix it by adding a mains-powered Zigbee router (plug/switch) nearby. LQI is unitless, so it lands in the default state measurement (not its own unit-named one); filter by the _lqi$ regex. The matching _rssi entities (in the dBm measurement) are also logged if you want a dBm view:
SELECT last("value") FROM "state"
WHERE "entity_id" =~ /_lqi$/ AND $timeFilter
GROUP BY "friendly_name" fill(none)
Zigbee Signal — LQI over time (time series) — the same _lqi$ data grouped by time($__interval) as well as friendly_name, so you get one line per device. Use it to watch a sensor re-parent (a step change in LQI) and confirm a new router actually helped — the bar gauge only shows the current snapshot. Sleepy end devices report sparsely, so spanNulls: true:
SELECT mean("value") FROM "state"
WHERE "entity_id" =~ /_lqi$/ AND $timeFilter
GROUP BY time($__interval), "friendly_name" fill(none)
Step 5 — Panel types to use¶
| Data | Grafana panel type |
|---|---|
| Temperature, CO₂, VOC, humidity, power | Time series |
| kWh totals | Stat |
| Garage doors, presence, leaks | State timeline |
Step 6 — Polish¶
- Set dashboard refresh to
30s - Set default time range to
Last 24 hours - Star the dashboard so it appears on Grafana home
- Optional: add Grafana as an iframe card in your Lovelace dashboard
- Use
$timeFilterandGROUP BY time($__interval)in every panel query — never hardcodetime > now()-24hor a fixedtime(5m)bucket, or the panel ignores the time picker and won't zoom.
Provisioning (how the dashboard is deployed)¶
The dashboard is file-provisioned, deployed via the ci.yml deploy-grafana job (see CLAUDE.md). The community Grafana add-on does not read /config/grafana/provisioning out of the box — two settings make it work:
- Grafana add-on config → env var
GF_PATHS_PROVISIONING=/config/grafana/provisioning. grafana/provisioning/dashboards/ha.yaml→options.path: /config/grafana/provisioning/dashboards(the add-on's default/etc/grafana/...path is empty — pointing there silently provisions nothing).
The provider polls every 30s (updateIntervalSeconds: 30), so a deployed file change goes live within ~30s with no restart. Debug with add-on env var GF_LOG_FILTERS=provisioning.dashboard:debug then ha addons logs a0d7b954_grafana (look for Start walking disk / saving new dashboard).
Retention & Downsampling (long-term data)¶
Two databases provide a tiered history (the foundation for long-running house analysis):
| Database | Retention | Resolution | Purpose |
|---|---|---|---|
homeassistant (autogen RP) | 365 days | full (~1/min) | detailed recent analysis, zoom to the minute |
homeassistant_downsample (5y_rollup RP) | 5 years | 1-hour | long-term trends |
Continuous query cq_hourly rolls raw data into the 5-year DB every hour, storing value (mean), value_max, and value_min per series — so long-term peak/extreme analysis is possible, not just averages:
CREATE CONTINUOUS QUERY cq_hourly ON homeassistant BEGIN
SELECT mean(value) AS value, max(value) AS value_max, min(value) AS value_min
INTO homeassistant_downsample."5y_rollup".:MEASUREMENT
FROM homeassistant.autogen./.*/ GROUP BY time(1h), * fill(none)
END
Two Grafana datasources are provisioned: InfluxDB — Home Assistant (raw, default) and InfluxDB — Long-term (the rollup). Point recent/detail panels at the raw datasource; point multi-year trend panels at the Long-term one. Storage is a non-issue — full-res raw is ~0.3–1 GB/yr, the hourly rollup ~30 MB/yr (disk is <5% used).
To change retention: ALTER RETENTION POLICY "autogen" ON "homeassistant" DURATION <e.g. 8760h> SHARD DURATION 168h DEFAULT (retention changes only affect data going forward / within the window — they can't recover already-expired data).