Adaptive comfort and forecast-aware heating control for Versatile Thermostat (VTherm), as a Home Assistant custom integration.
Hearth sits outside VTherm and drives it through public entities only:
it writes the per-preset temperature numbers (number.<vtherm>_preset_comfort_temp
and friends) and switches presets with climate.set_preset_mode. It never writes a
raw setpoint. Disable Hearth and VTherm carries on with the last preset temperatures.
Four mechanisms, delivered as four phases, each behind its own switch:
| Phase | Mechanism | Default |
|---|---|---|
| 1 | Adaptive comfort offset: the comfort target drifts with a running mean of outdoor temperature (EN 16798 / ASHRAE 55 acclimatisation) | on |
| 2 | Forecast skip and setback depth: skip the morning heat when the day will do it for free; shallower overnight setback before very cold mornings | off |
| 3 | Warm-by schedule with learned preheat: blocks say when a room should be warm; Hearth learns how fast each room warms and starts early enough | off |
| 4 | Override learning: manual changes are recorded in a correction ledger (record-only; application is a separate flag, off) | off |
Status: early. All four mechanisms are implemented and covered by tests against a simulated VTherm, but Hearth has not yet completed a heating season on a live system. Start with phase 1 on one room and keep an eye on it. Hearth is an independent project, not affiliated with Versatile Thermostat.
See DECISIONS.md for what was chosen where the design left options open, and BUILD-REPORT.md for what is done, untested and worth checking on a live system.
HACS (custom repository)
- HACS > Integrations > three dots > Custom repositories
- Add
https://github.com/Bibbleq/HA-Integration-Hearth, category Integration - Install Hearth, restart Home Assistant
Manual: copy custom_components/hearth into your config/custom_components/ and restart.
Then Settings > Devices & services > Add integration > Hearth. One Hearth room per VTherm.
- VTherm 8.0 or newer (built and verified against 10.4).
- The VTherm must expose its own preset temperature numbers. In the VTherm's config, untick
"use central configuration" for preset temperatures; otherwise
number.<vtherm>_preset_comfort_tempdoes not exist and Hearth reports the room as dormant (no_preset_entities). - Presence, window, safety and overpowering stay VTherm's job. Hearth is dormant while any of them is active.
- The TRV side must be a plain actuator with no early-start or smart schedule of its own (on a Tado estate: flat temperatures in the Tado app, Early Start off). Hearth's preheat must be the only warm-up brain.
Tier 1, runtime entities (per room unless noted): change them from the UI at any time.
| Entity | Default | Purpose |
|---|---|---|
switch.hearth_active |
on | Global kill switch (one for the whole integration) |
switch.hearth_<room>_adaptive_comfort |
on | Phase 1 |
switch.hearth_<room>_forecast_skip |
off | Phase 2 |
switch.hearth_<room>_setback_depth |
off | Phase 2 |
switch.hearth_<room>_schedule |
off | Phase 3: Hearth owns preset switching for this room |
switch.hearth_<room>_preheat |
off | Phase 3: warm-by blocks start early (needs schedule on) |
switch.hearth_<room>_override_learning |
off | Phase 4: record overrides in the ledger |
switch.hearth_<room>_apply_learned_corrections |
off | Phase 4: apply bounded corrections from the ledger |
number.hearth_<room>_adaptive_slope |
0.15 | C of comfort per C of running mean above/below 10 C |
number.hearth_<room>_skip_threshold |
18 C | Forecast high at or above which the morning heat is skipped |
number.hearth_<room>_max_offset |
1.5 C | Cap on the total adaptive offset |
number.hearth_<room>_min_indoor_floor |
16 C | Below this, no skip starts and any active skip aborts |
number.hearth_<room>_cold_morning_threshold |
2 C | Coldest 06:00-09:00 forecast hour below which the overnight setback is shallower |
Tier 2, config entry options (Configure on the integration): target VTherm, room name, weather
entity, outdoor sensor override, workday sensor, baseline comfort temperature, comfort band min/max (the hard clamps),
affected presets, baseline boost temperature, solar-gain flag, skip decision / skip end / bedtime decision
times. The schedule (phase 3) is also stored here, edited via the hearth.set_schedule service.
Tier 3, constants: everything else lives in one DEFAULTS dict in const.py. Any key can be
overridden from configuration.yaml:
hearth:
advanced:
alpha: 0.8 # running-mean weight
t_ref: 10.0 # C, running mean at which no offset applies
standdown_timeout_min: 180
max_forecast_age_hours: 3
safety_factor: 1.15
max_preheat_min: 120Unknown keys are ignored with a warning. Nothing in Tier 3 is required for sane behaviour.
Each room is a device named Hearth <Room>. Diagnostics (read-only):
| Entity | Meaning |
|---|---|
sensor.hearth_<room>_running_mean_outdoor |
T_rm in C, with today's partial mean, sample count and recent daily means |
sensor.hearth_<room>_comfort_target |
Computed comfort target (pre-quantise) with the applied offset, quantised value and last write |
sensor.hearth_<room>_skip_status |
idle / preview / active / aborted, with reason, threshold and forecast basis |
sensor.hearth_<room>_skip_preview |
Today until the decision time, then tomorrow: likely / unlikely / unknown, with the forecast basis |
sensor.hearth_<room>_setback_status |
idle / active, with the coldest-morning forecast, eco base and target, restore time |
sensor.hearth_<room>_warming_rate |
C/h at the current outdoor temperature, with model constants and run count |
sensor.hearth_<room>_next_block |
Phase 3: "comfort by 06:30, preheat est. 05:52" |
sensor.hearth_<room>_stand_down |
none / until (local timestamp), with cause and the last external change |
sensor.hearth_<room>_learning_<bucket> |
Phase 4: decayed mean bias in C per bucket (preheat_shortfall, slope_error, skip_failure, baseline_error) with evidence count and the correction that would apply |
binary_sensor.hearth_<room>_dormant |
On when Hearth is leaving the room alone, with reason (away / central_frost / window / safety / overpowering / off / unavailable / disabled / no_preset_entities) |
Hearth also fires hearth_diagnostic events (seeding, outdoor sensor loss, refused writes) and provides a
config-entry diagnostics download.
| Service | Fields | What it does |
|---|---|---|
hearth.override |
room, duration |
Stand Hearth down deliberately ("heat normally for 3 h"). Ends an active skip and restores the preset. Works whether or not override learning is on |
hearth.recompute |
room (optional), force |
Force an adaptive recompute and write |
hearth.refresh_forecast |
room (optional) | Fetch the forecast now |
hearth.set_schedule |
room, schedule |
Replace a room's warm-by schedule (phase 3) |
hearth.reset_learning |
room, bucket (optional) |
Clear the ledger, or one bucket (phase 4) |
room is either entry_id (config entry selector) or vtherm_entity_id (the climate entity the room drives).
Phases are meant to soak for a few weeks each. Everything is per room.
- Adaptive comfort is on from install. For the first 48 h after seeding the offset is held at zero.
Watch
sensor.hearth_<room>_comfort_targetagainst the VTherm's comfort number. - Forecast skip / setback: configure a weather entity (options), then turn on
switch.hearth_<room>_forecast_skipand/orswitch.hearth_<room>_setback_depth. Your existing HA Scheduler entries stay in charge of presets; a Scheduler slot firing mid-skip ends the skip. Checksensor.hearth_<room>_skip_previewin the evening andskip_statusin the morning. - Schedule and preheat: set the room's schedule with
hearth.set_schedule, turn onswitch.hearth_<room>_schedule, then disable that room's HA Scheduler entries (the only hand-over needed when the TRVs are plain actuators). Once the preset switching looks right, turn onswitch.hearth_<room>_preheat. The warming-rate model uses a conservative 1.0 C/h until it has five clean heating runs. - Override learning: turn on
switch.hearth_<room>_override_learning. From then on a manual preset or temperature change stands Hearth down for that room (until the next schedule block or 180 min) and is recorded in the ledger. Thelearning_*sensors show the biases and the correction each bucket would apply.apply_learned_correctionsis a second-winter flag: bounded corrections (+/- 1 C on targets, +/- 25 % on the preheat safety factor) after three consistent events, always inside the band clamps.
service: hearth.set_schedule
data:
vtherm_entity_id: climate.living_room
schedule:
mon: &weekday
- { warm_by: "06:30", preset: comfort, skippable: true }
- { at: "09:00", preset: eco }
- { warm_by: "17:00", preset: comfort }
- { at: "22:30", preset: eco }
tue: *weekday
wed: *weekday
thu: *weekday
fri: *weekday
sat: &weekend
- { warm_by: "08:30", preset: comfort, skippable: true }
- { at: "22:00", preset: eco }
sun: *weekend
# non_workday: optional, see belowwarm_by blocks get preheat; at blocks fire at the stated time. skippable marks blocks the forecast
skip may suppress. Presets: frost, eco, comfort, boost. One year-round schedule per room; the
adaptive offset, skip and preheat handle the seasons.
Bank holidays. Set a Workday integration sensor in the room options. A Monday to Friday that the
sensor says is not a workday uses the schedule's non_workday blocks if it has them, otherwise
Saturday's. Without a workday sensor every date follows its weekday.
Frost in a schedule is normal. Hearth stays active through frost blocks it applied, and a frost preset set by hand stands until the next block like any other manual change. Only VTherm's central "Frost protection" mode (holiday) makes Hearth dormant. Eco is the better resting state between comfort blocks: recovering from frost can take longer than the 120 minute preheat cap allows.
- Every preset temperature write goes through one clamped writer. Comfort and boost never leave
[band_min, band_max]; eco is never lowered by Hearth and never raised aboveband_maxor the comfort target. - A stale forecast (older than 3 h) disables skip and setback decisions; heating proceeds normally.
- Outdoor sensor loss freezes the running mean and holds the offset.
- VTherm presence away, central frost protection, window, safety or overpowering makes Hearth dormant for that room.
min_indoor_floorbreach cancels any Hearth-originated eco state immediately.- Restart is recompute-from-state: Hearth never re-fires actions blindly.
Pure maths lives in custom_components/hearth/core/ with no Home Assistant imports and runs under plain
pytest. The HA glue tests need pytest-homeassistant-custom-component (see requirements_test.txt).
CI runs pytest, hassfest and HACS validation on every push.