Skip to content

HVAC Runtime-vs-Temperature Characterization (WS2 summer slice)

Date: 2026-07-07 Ticket: #29 — Energy WS2: finish HVAC characterization Status: Approved (design settled via brainstorm)

Goal

Deliver the data-ready, summer-appropriate slice of WS2:

  1. A one-time diagnostic that computes, per AC condenser, how strongly daily runtime correlates with the daily outdoor high — flagging any system whose runtime does not track temperature (possible stuck relay / thermostat fault / refrigerant problem).
  2. Live Grafana artifacts on the Home Intelligence dashboard: a runtime-vs-outdoor-high scatter and per-floor runtime stat cards.

The remaining WS2 tasks (heat-pump crossover, simultaneous heat/cool conflict, gas-furnace blower draw) are winter/season-gated and explicitly out of scope here.

Data availability — VERIFIED (2026-07-07)

Confirmed by direct InfluxQL query against the live InfluxDB (db=homeassistant, add-on a0d7b954-influxdb):

Series Measurement Field Tag Daily reduction Window confirmed
Outdoor high state temperature entity_id='forecast_home' max() GROUP BY time(1d) 2026-06-12 → 07-07 (26 d; heat wave 100/101/101°F Jul 2–4)
Condenser runtime (×3) h value entity_id='{first,second,third}_floor_condenser_runtime_today' max() GROUP BY time(1d) 2026-06-13 → 07-07 (25 d; 4–11 h/day)

weather.forecast_home has no unit_of_measurement on its (string) state, so default_measurement: state (influxdb_ha.yaml) routes its numeric temperature attribute into the state measurement. The entity_id tag carries no sensor. prefix (house convention). Both series exceed the ≥21-day bar the ticket requires. No new HA entities are needed — the runtime booleans/history_stats sensors and the weather integration already exist.

Existing infrastructure reused (no change)

  • binary_sensor.{first,second,third}_floor_condenser_running (>200 W threshold)
  • sensor.{first,second,third}_floor_condenser_runtime_today (history_stats, hours, midnight reset)
  • sensor.em_condenser_floor{1,2,3}_power (raw watts)
  • weather.forecast_home (outdoor temp / forecast)
  • Grafana HVAC row with the live "HVAC Power — Condensers + Air Handler" panel

Component 1 — One-time diagnostic script

File: scripts/analysis/hvac_runtime_correlation.py (repo-committed, run manually — not automated).

What it does: 1. Reads InfluxDB connection from the environment (host/db/user/password), mirroring how CI's Grafana-verify step authenticates. Queries the two daily series above for all three condensers over the full available window. 2. Pairs runtime↔outdoor-high by day (inner join on the GROUP BY time(1d) buckets; drop any day missing either value — e.g. the partial install day). 3. Computes per condenser: Pearson r, OLS slope (hours runtime per °F outdoor high), intercept, sample size n, and the outdoor-high range covered. 4. Verdict per condenser: OK if r ≥ 0.4, else FLAG (weak/no temperature coupling — the ticket's stuck-relay / thermostat / refrigerant signal). In a July cooling season a healthy AC should show a strong positive r. 5. Prints a summary table to stdout and writes the findings (r / slope / verdict per condenser, plus the analysis window) into projects/house_energy_strategy.md → WS2.

Dependencies: Python ≥3.11 + requests (InfluxDB HTTP API) and stdlib statistics (statistics.correlation for Pearson r, 3.10+; statistics.linear_regression for slope, 3.11+) — no numpy/pandas. Keep the dependency surface minimal so it runs anywhere the repo is checked out.

Threshold: r < 0.4 ⇒ FLAG. A single documented constant at the top of the script; adjust in one place if real data argues for stricter/looser.

Component 2 — Grafana live artifacts

Edited in grafana/dashboard.json (source of truth; deployed via CI deploy-grafana, file-provider auto-reload ~30 s, no HA restart). Added to the existing HVAC row.

2a. Runtime vs. Outdoor-High — dual-axis timeseries panel

Chosen over a literal x-y scatter because the xychart panel's manual x/y mapping is schema-fragile on a provisioned, file-managed Grafana 12 dashboard (no UI to nudge it) and the guard tests can't catch a mis-mapped render. The quantitative correlation (Pearson r + slope) is delivered by the Component-1 diagnostic; this panel's job is the glanceable "does runtime track temperature?" read, which a dual-axis timeseries shows reliably.

  • Queries (4): three condenser daily-runtime (max("value") from h, per *_condenser_runtime_today, GROUP BY time(1d)) + one outdoor daily-high (max("temperature") from state, entity_id='forecast_home', GROUP BY time(1d)). All use $timeFilter and fill(none).
  • Panel: reuse the exact schema of the existing "HVAC Power — Condensers (per floor)" timeseries panel (id 35) — proven, guards-clean. Left Y-axis = runtime hours (3 series, bars draw style, one colour per condenser); right Y-axis (field override on the outdoor-high series) = °F as a line.
  • Time range: dashboard default; meaningful from ~21 days onward.

2b. Per-floor runtime stat cards — 3 × Stat panel

  • One Stat panel per condenser: today's runtime (last value of *_condenser_runtime_today, measurement h) as the big number (unit: hours), with a daily-history sparkline graph mode behind it.
  • Colour thresholds left neutral (informational, not alerting — short-cycle alerts already exist in automations.yaml).

Grafana guards (Tier-0 tests enforce): real datasource UID (no ${DS_INFLUXDB}), no now()-24h literal, valid JSON. New panels follow the existing panels' datasource-UID pattern.

Testing

  • make test — Tier-0 lint/pytest covers JSON validity + Grafana datasource guards; no new HA entities so Tier-3 entity-ref is unaffected. The diagnostic script is not part of the deploy gate (analysis tool), but must run cleanly end-to-end before findings are recorded.
  • Manual verification after deploy-grafana: the dual-axis timeseries renders runtime bars + outdoor-high line tracking together, three stat cards show plausible hours + sparklines, panels resolve their datasource (no "datasource not found").

Docs

  • projects/house_energy_strategy.md — record the correlation findings under WS2 and check off: "Build runtime vs. outdoor temperature correlation for each condenser" and "HVAC health dashboard: per-floor runtime stat cards".
  • docs/grafana.md — document the two new panels + their InfluxQL queries (mirrors how the existing ⚡ Energy / HVAC panels are documented).
  • No user-manual (docs/*.md household) page — this is an analytical/diagnostic surface, not a household-facing feature.

Out of scope

  • Winter/season-gated WS2 items: heat-pump crossover temperature, simultaneous heat/cool conflict detection, gas-furnace blower-draw characterization (em_attic_furnace_power is office-contaminated per #26; em_crawl_furnace_power clean but idle in summer).
  • #35 — the physical panel walk to identify the C4 phantom load (separate ticket, hands-on).
  • Any HVAC control — Ecobee owns heating/cooling; this is observation only.
  • Runtime-normalized-by-sqft comparison — deferred; not required for the health-check goal.

Acceptance criteria

  • [ ] scripts/analysis/hvac_runtime_correlation.py runs against live InfluxDB and prints per-condenser r, slope, n, and OK/FLAG verdict.
  • [ ] Findings recorded in projects/house_energy_strategy.md WS2; the two WS2 checkboxes ticked.
  • [ ] grafana/dashboard.json gains the dual-axis runtime-vs-temp timeseries + 3 runtime Stat panels in the HVAC row.
  • [ ] make test passes (JSON valid, Grafana guards green).
  • [ ] After deploy, panels render live with resolved datasources (screenshot check).
  • [ ] docs/grafana.md updated for the new panels.