Thermal State of Charge (TSOC) for Home Assistant
A building stores heat like a battery. Why don't we expose its state of charge?
It's a heatwave. You turn on the AC at noon and it runs for hours. The bedroom still feels hot at midnight.
The issue isn't the AC. It's the walls.
A concrete apartment that's been baking since 6am has walls at 29°C — even if the air reads 23°C. The AC cools the air. The walls keep radiating heat back. You're fighting inertia you can't see.
What's missing from every smart home setup today:
- How thermally loaded are my walls right now?
- Will the AC actually make a dent tonight, or has the building already absorbed too much?
- Should I start pre-cooling at 10am before the sun hits the west facade?
Current thermostats react to air temperature. But comfort is governed by wall temperature — and walls respond on a timescale of hours, not minutes.
Borrowed directly from battery management systems:
SoC = 0% → walls cold, comfortable day ahead without HVAC
SoC = 50% → walls loaded, HVAC needed but manageable
SoC = 100% → walls saturated, HVAC alone won't recover in time
This gives you three new virtual sensors:
sensor.thermal_soc # 0–100% — how charged are the walls right now?
sensor.thermal_decay_rate # °C/h — how fast does the building cool naturally?
sensor.hours_to_target # hours — how long until comfort target is reached?And enables automations that were previously impossible:
- if: sensor.thermal_soc > 80
and: weather.forecast_tomorrow > 35°C
then: start pre-cooling at 06:00The battery analogy runs surprisingly deep:
| Battery concept | Building equivalent |
|---|---|
| State of Charge (%) | Thermal SoC (%) |
| Capacity (Wh) | Thermal capacity (°C × thermal mass) |
| Discharge rate | Thermal decay rate (°C/h overnight) |
| Charging (solar, grid) | Solar gain, internal loads, outdoor heat |
| Discharging (load) | Night flushing, HVAC, ventilation |
| Resting voltage | Indoor equilibrium temperature |
Battery management systems expose SoC as a first-class value. No building management system does this — yet the concept maps perfectly.
Layer 1 — No Python required (native HA templates)
Computable from sensors you already have:
sensor.t_indoor_mean_24h: # 24h indoor average — proxy for wall load
sensor.night_cooling_delta: # °C drop between 2am and 7am
sensor.thermal_soc: # empirical combination → 0–100%Layer 2 — EWA estimator (more accurate)
An Exponentially Weighted Average tracks estimated wall temperature:
T_walls(t) = T_walls(t-1) + (dt / τ) × (T_air(t-1) - T_walls(t-1))
Two parameters, calibrated once per building:
τ_walls— thermal time constant (hours). How slowly do your walls respond?roof_coupling— constant heat flux from top floor or roof (°C/h). Zero for ground/middle floors.
Layer 3 — Predictive script
Solves the inverse problem: "when do I need to start HVAC to reach comfort by my deadline?"
- Concept validated
- EWA calibrator (
src/calibrate.py) - Passive monitor (
src/monitor.py) - Wall simulation plots (
src/visualize.py) - Calibrated on one apartment (concrete, top floor, Alpine foothills) — τ_walls=5.2h, roof_coupling=0.261°C/h
- Layer 1 HA template sensors (YAML) — running natively in production, zero Python, 24/7
- Prediction/reality reconciliation layer — forecast-based projection cross-checked against live measurement (see field validation below)
- Validated on multiple building types
- HA custom component (HACS)
This is early-stage research. We have one calibrated building running the model continuously in production, and a working calibration pipeline. We need more buildings to validate the concept.
The HA-native version (Layer 1 — template sensors + automations, no Python) has been running for about 10 days on Apartment A. It just caught something worth sharing.
The "recommend AC start time" sensor combines two inputs: the live EWA-estimated wall temperature, and today's forecasted outdoor max — pulled once in the morning from the weather integration's hourly forecast and expected to stay fixed for the rest of the day.
It doesn't. The extraction logic scans the weather integration's remaining hourly forecast entries for "today, 7am–10pm." By early evening, none of those hours are in the future anymore — the forecast API only ever returns what's ahead of now. The scan finds nothing, and the template silently falls back to a hardcoded default instead of keeping the value it correctly computed that morning. Every evening, any downstream sensor relying on "today's forecast max" quietly got corrupted — no error, no state that looks obviously wrong, just a wrong number feeding a real decision.
This is a general trap for any HA template that reasons about "today's forecast" late in the day using a rolling hourly forecast entity: the data window for "today" runs out before the day does.
Fix, in two parts:
- Preserve the last known-good value instead of resetting to a magic constant when the scan finds nothing.
- More importantly for TSOC specifically: stop trusting the forecast alone. A second helper now tracks the actual observed outdoor max so far today (ratcheting, reset at midnight), and every downstream calculation uses
max(forecast, observed). Abinary_sensorflags when live conditions already exceed the morning forecast by more than a configurable margin — which is exactly the situation that exposed this bug (a mild-looking morning forecast, a warmer-than-expected real day).
Small bug, but a clean illustration of the project's core argument: forecast-only, "compute once and trust it" logic is fragile. Reconciling prediction against live measurement — the same principle behind the EWA model itself — is what catches this kind of silent drift.
Concrete construction, 4th/5th floor, Alpine foothills. Summer 2026.
| Parameter | Value | How measured |
|---|---|---|
base_heating_rate |
+0.22 °C/h | Passive drift, windows closed, no HVAC |
hvac_rate (favourable evening) |
−2.5 °C/h | HVAC at 22°C setpoint, T_ext ~22°C |
hvac_rate (heatwave, sun on facade) |
−0.84 °C/h → plateau | HVAC at 22°C, T_ext 31°C, afternoon sun |
solar_gain (NE bedroom, direct) |
+0.55 °C/h | Morning sun, door closed |
thermal_floor (heatwave, HVAC on) |
23.4–23.6 °C | Minimum reachable with HVAC at 22°C, T_ext min 22°C |
roof_coupling |
0.261 °C/h | EWA calibration, 499 pts over passive nights (top-floor heat flux) |
τ_walls |
5.2 h | EWA calibration, passive nights, T_ext min 14°C |
The thermal_floor finding is key: in a heatwave with T_ext minimum 22°C, this apartment cannot reach below 23.4°C even with continuous HVAC. The walls — and the floor above — won't allow it. Knowing this in advance changes how you plan.
The best thermal model won't come from physics textbooks. It will come from observing many real buildings.
To contribute your building's calibration data:
- Run
src/monitor.pyfor 48h — passive, no HVAC intervention - Run
src/calibrate.pyto extract your parameters - Open a PR adding your results to
data/buildings/
Useful metadata (no personal data required):
- Building type: concrete / timber frame / stone / brick
- Construction year
- Floor position (ground / middle / top)
- Orientation of main rooms (N/S/E/W)
- External insulation? (yes/no, approx thickness)
- Climate zone (Köppen: Cfb, Csa, Dfb…)
Each calibration makes the collective model stronger.
git clone https://github.com/flowcool/ha-thermal-soc
cd ha-thermal-soc
pip install -r requirements.txt
cp config/example.yaml config/my_home.yaml
# Edit config/my_home.yaml with your entity IDs
# Run passive monitor (logs to data/passive_log.csv)
HASS_TOKEN=xxx python src/monitor.py --config config/my_home.yaml
# After 24-48h of passive data, calibrate
HASS_TOKEN=xxx python src/calibrate.py --config config/my_home.yaml
# Or calibrate from local CSV
python src/calibrate.py --config config/my_home.yaml --csv data/passive_log.csvsrc/
monitor.py # Passive temperature logger — CSV output, no HVAC control
calibrate.py # EWA parameter fitting from HA history or CSV
visualize.py # Wall charge/discharge simulation plots
config/
example.yaml # Configuration template — copy and adapt
data/
buildings/ # Anonymized calibration results per building (PRs welcome)
- Python ≥ 3.10
- Home Assistant instance with per-room temperature sensors + outdoor temperature
requests,numpy,scipy,matplotlib,pyyaml
MIT — use freely, contribute back.