Skip to content

Grafana Analytics Setup

InfluxDB version: 1.x (Chronograf UI)


Step 1 — InfluxDB: Create Database & User ✅

In Chronograf (InfluxDB Web UI):

  1. Click crown icon (Admin) → InfluxDB tab → Create Database
  2. Name: homeassistant
  3. Click Users tab → Create User
  4. Username: homeassistant
  5. Grant access to homeassistant database

Step 2 — Wire HA to InfluxDB

In secrets.yaml (HA config root), add:

influxdb_password: <your-influxdb-password>

In configuration.yaml (HA config root), add:

influxdb: !include influxdb_ha.yaml

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

  1. Open Grafana (HA → Add-ons → Grafana → Open Web UI)
  2. Go to Connections → Data Sources → Add new data source
  3. Select InfluxDB
  4. Set:
  5. Query Language: InfluxQL (NOT Flux — that's for v2)
  6. URL: http://localhost:8086
  7. Database: homeassistant
  8. User: homeassistant
  9. Password: (your InfluxDB password)
  10. Click Save & Test — should show green ✓

Step 4 — Build the Dashboard

  1. Grafana → Dashboards → New Dashboard → Add visualization
  2. Select the InfluxDB data source
  3. 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)
These three live in the 🌐 Internet row as separate panels (Download/Upload use unit 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)

SELECT "value" FROM "state"
WHERE ("entity_id" =~ /person.louis|person.lindsay/)
AND $timeFilter

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):

SELECT last("value") FROM "$" WHERE "entity_id" = 'energy_cost_today' AND $timeFilter

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; reducesortBy 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_name tag — 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)
The "Outdoor High" series overrides to a line on the right axis (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
Returns one point per day; 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)
These LQI/RSSI diagnostic entities are disabled by default in ZHA — they were enabled across all Zigbee devices to feed this panel.

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 $timeFilter and GROUP BY time($__interval) in every panel query — never hardcode time > now()-24h or a fixed time(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:

  1. Grafana add-on config → env var GF_PATHS_PROVISIONING=/config/grafana/provisioning.
  2. grafana/provisioning/dashboards/ha.yamloptions.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).