How to point it at your Home Assistant, run it, and read every chart it draws. Written to be read cold, with no prior knowledge of the project assumed.
solarGain answers one question with your own sensor data: how much is the sun heating your house, and what is that costing you in air conditioning? It pulls temperature, solar production, wind, and AC power history out of Home Assistant's recorder, then renders it all as a single dashboard.html you can open anywhere. The file carries its data inside it. No server, no chart library, no npm install, no cloud.
It was built for a specific problem: a one-story house with a hot attic, portable AC units fighting that attic every evening, and a homeowner who wanted numbers before buying a radiant barrier, a ridge vent, or blown-in insulation. The dashboard fits a small thermal model to your own history and shows what each of those upgrades would plausibly do, using your roof, not a marketing brochure.
What it does not do: it does not control anything (every request is a read-only GET), it does not update live (each run is a snapshot; run it again for fresh data), and it does not phone home.
The dashboard is one page, top to bottom: a time-range filter, a row of stat tiles, a stack of chart cards (drivers, attic heat load, three what-if scenarios, a wind check, room-by-room panels), and a plain-language "What the data says" list at the end.
purge_keep_days) is 10 days, "All" means 10 days.sensor.attic_environment_temperature). solarGain finds temperature sensors by friendly name: exact match first, then a unique substring match. AC power sensors are the exception; they are pinned by exact entity ID..env file and never in git.You need Python 3.9 or newer (for zoneinfo; macOS and any recent Linux qualify) and a Home Assistant instance you can reach on your network. There is nothing to pip install. The whole pipeline is standard library.
cd into it.cp entities.example.py entities.py (editing it is section 4).cp .env.example .env
# then edit .env:
HASS_TOKEN=eyJhbGciOi...
HASS_URL=http://homeassistant.local:8123HASS_URL. If you skip it, the fetch script falls back to http://homeassistant.local:8123, which works on many networks but not all; an IP address is more reliable. .env is gitignored; keep the token out of commits.Want to see the dashboard before creating a token? ./run.sh --mock builds it from synthetic data with the expected physics baked in (a diurnal outdoor sine, a solar bell curve, a lagged attic). It is how the charts were developed before real data existed.
entities.py is the single source of truth for what gets fetched. It describes your house, so it lives outside git (like .env): copy the tracked template and edit the copy.
cp entities.example.py entities.py
Three maps matter:
TEMPERATURE_SENSORSOne entry per sensor. Each has a match (the friendly name to resolve), a label (what the chart shows), and a group: driver for outdoor and attic, room for living spaces, reference for no-attic control zones. A room entry may also carry an ac key naming the AC unit that serves it; that is what links run bands to room panels. Two rooms can share one unit (an open floor plan kitchen and living room, say).
Minimum viable set: outdoor, attic, and at least one room. The scenario cards need outdoor, attic, and solar to fit anything.
SOLAR_SENSORS and WIND_SENSORSCurrent solar production in kW (any inverter integration that exposes it) and wind speed in mph (a weather station, or drop it if you have none; the wind card just stays empty).
POWER_AC_SENSORSFor AC units on energy-monitoring smart plugs. Point each entry at the plug's cumulative daily energy sensor (the "Energy Today" kind that climbs all day and resets at midnight), by exact entity ID. The fetcher differentiates that curve into watts, then marks the unit "running" wherever sustained draw exceeds threshold_w (150 W by default, comfortably above standby and fans, comfortably below any compressor). Gaps under 30 minutes get bridged so a cycling compressor reads as one run, not twenty.
WARN: could not uniquely resolve line means a friendly name matched zero entities or several; fix the match string until the warnings are gone. The dashboard silently skips series it never received, so a clean fetch log is worth thirty seconds of your attention.Two more edit points live in fetch_history.py: LOCAL_TZ (ships as America/Los_Angeles; set your own or every "Today" filter and daily stat is shifted) and DEFAULT_URL (or just set HASS_URL in .env and ignore it).
./run.sh # fetch, build, open dashboard.html
./run.sh --days 3 # smaller fetch window
./run.sh --mock # synthetic data, no token needed
dashboard.html is a snapshot you can email, airdrop, or archive; it will render identically in ten years.| Flag | Default | What it does |
|---|---|---|
--days N | 10 | How many days of history to request. Asking for more than your recorder keeps is harmless; you get what exists. |
--mock | off | Skip Home Assistant entirely and generate synthetic data. Useful for previewing charts and for hacking on the template. |
What lands on disk: data/history.json (the snapshot) and dashboard.html (the deliverable). Both are gitignored, because both contain your house's data. Re-running is always safe; each run overwrites both files and touches nothing else.
Range filter. Five presets: Today, 24 h (the default), 3 days, 7 days, All. Every chart, tile, and insight on the page recomputes for the selected window. Times are your configured timezone, not UTC, so "Today" starts at your local midnight.
Stat tiles. The headline numbers for the window:
Every card has a table button in its corner that swaps the chart for the underlying numbers (hourly means). Same data, no chart. Useful for screen readers, spreadsheets, and settling arguments.
Outdoor temperature, attic temperature, and solar production on the same clock. Temperature and kW never share an axis; solar sits in its own aligned panel below, so you can see cause above effect without a dual-axis chart lying to you about scale. Watch a sunny day: solar peaks, then the attic peaks an hour or two later, then radiates into the evening long after the outside air has cooled.
The same story reduced to one line: how many degrees the roof adds on top of the weather. Zero means the attic is just tracking outside air (typical pre-dawn). The daily hump is pure solar gain. This is the line every upgrade is trying to flatten.
Each fits a model to your measured history, then replays the hottest day in the window with one change. Modeled lines are long-dashed with a shaded uncertainty band; the measured attic line stays solid next to them for honest comparison. The fit statistics (R², time constant) print under each chart, so you can judge whether the model earned an opinion.
A scatter of model residuals (how much hotter the attic ran than solar predicts) against wind speed, during solar hours. A downhill slope means breeze helps your attic; a flat line means your vents cannot exhale, no matter the wind. Flat is a useful answer: it says buy intake and exhaust area before buying a fan.
One small panel per room, all sharing a single y-axis so rooms compare honestly. Reference zones are labeled "(no attic)".
The last card turns the window's numbers into sentences: how much the roof added on top of the weather, the attic's lag behind the sun, when outdoor air first dropped below your average room temperature each evening (the moment opening windows starts working), and how your AC energy overlapped solar production. It recomputes with the range filter like everything else.
./run.sh, and the build timestamp is printed in the header. If it looks stale, it is; run it again.HASS_URL you configured.WARN lines. A friendly name that matches zero or multiple entities gets skipped, and the dashboard draws what it received without complaint.POWER_AC_SENSORS. Units known only through a climate entity get runtime from state history instead, which trusts the integration more than a watt meter would.Companion guide to solarGain. If it behaves differently from this guide, the code is the source of truth.