Pre-release
Cooling finally works the way heating does, doors join windows, Z-Wave thermostats are supported, there's a new control algorithm to try, and a very long list of small annoyances is gone.
Nothing to do after updating. Your existing setup keeps working — no reconfiguration, no migration steps. Except you relay on new options.
Thanks to everyone who reported, tested and fixed things, and special thanks to @cl445.
❄️ Cooling and air conditioning
Cooling used to be the weak spot. This release rebuilt it.
- Air conditioners with a heat/cool range (a lower and an upper temperature instead of one setpoint) are handled properly now, including units that only accept both values together.
- Separate cooling temperatures for presets — Eco, Away or Comfort can mean one temperature in winter and another in summer.
- Cooling waits out the tolerance instead of switching on the moment the room is a hair too warm. Fewer short bursts.
- Changing the temperature on the AC itself or its remote is picked up correctly, instead of being mistaken for our own last command.
- If the air conditioner disappears from Home Assistant, you get a clear warning instead of silent guessing.
- Far fewer commands sent to cloud-connected air conditioners — matters for brands that rate-limit or charge per request.
- Cooling reports
cooling when it is actually cooling.
We'll working on the cooling options in v2, specially on combined devices for the next summer.
🚪 Doors, on top of windows
- Door sensors can pause heating just like window sensors, with their own open and close delays — a front door counts differently from a balcony window.
- An open window or door now stops cooling too, not just heating.
- Rooms with both heating and cooling behave sensibly while a contact is open.
🧠 A new control algorithm to try
- MPC v2 — a new, more advanced calibration mode that looks ahead instead of only reacting. Pick it per thermostat in the configuration.
- Ships with room-size presets (small / medium / large room, or automatic), so there is nothing to tune by hand.
- The existing modes are untouched. Switch back any time.
📡 Z-Wave thermostats
- Z-Wave JS thermostats are supported directly, including valve control for the Eurotronic Spirit and the Aeotec ZWA021.
🔧 Better behaviour with stubborn thermostats
Many devices lie a little, round differently, or refuse commands. 1.9 handles that.
- We only send operating modes your thermostat actually offers, instead of sending something it will reject.
- Temperatures are compared in the steps your device really uses (0.5 °C, 1 °C, whole degrees), so we stop re-sending a value that is already set.
- Fahrenheit and mixed-unit setups read correctly.
- Devices with two temperature values, and devices that report their capabilities late, are detected more reliably.
- Calibration offsets a device silently ignored are sent again instead of being assumed done.
- Error messages name the setting you actually need to change.
🛠️ Valve care
- Valve exercising also runs while the thermostat is switched off — exactly when valves get stuck.
- Sleeping or unreachable thermostats are skipped instead of holding up the whole cycle.
- After the anti-sticking movement, the valve returns to the position it should be in.
- Valve commands stay inside the range and step size the device declares (noticeably better on Sonoff TRVZB).
- New service to trigger valve maintenance yourself, whenever you want.
🚀 Startup and reliability
- Missing or slow sensors during Home Assistant startup no longer produce a wave of errors — we wait, retry, and only then report a problem.
- New degraded mode notice tells you which sensor is missing and what we're doing meanwhile. Clears itself once the sensor is back.
- Removing a thermostat — or just the air conditioner from an existing configuration — works cleanly.
- Nonsense values from a sensor or from a previous restart can no longer poison the control loop.
🏠 Home Assistant integration
- Better Thermostat events show up in the Activity Log timeline, and the real hardware behind a Better Thermostat is shown on its device page.
- New diagnostic sensors for what the PID calibration is doing — useful if you like tuning.
- Better Thermostat entities work in the device-based automation editor (triggers, conditions, actions).
🌍 Setup screen, presets and translations
- Setup and options screens are fully translated, including the dropdowns and entity names that were English-only before. All eleven shipped languages brought up to date.
- Changing the target temperature now leaves the preset instead of pretending it is still active.
- Each thermostat in a group can use its own temperature step, so mixed-brand groups stop fighting each other.
- A temperature set from the app, a dashboard or an automation is no longer swallowed when it arrives at the same moment as an update from the device.
📋 Automations (blueprints) Fixes
- The weekly heating schedule blueprint saves again.
- It accepts several presence entities and several pause switches, and no longer resumes heating while another pause switch is still on.
- Older saved configurations keep working.
📖 Documentation
- New pages: presets explained, window sensors, degraded mode, and running Better Thermostat alongside a central boiler thermostat.
- The algorithm guide was rewritten to describe what each mode actually does, without promising results the project cannot back up.
- Outdated content, dead links and broken images removed.
💾 Installation Fixes
- Better Thermostat installs on Raspberry Pi and other ARM systems again — an optional maths component is no longer required.
🧪 Under the hood
Large parts of the internals were rewritten and are now covered by an automated test suite that grew several times over during this release, plus a simulation benchmark that compares the control algorithms against each other. You won't see any of it directly — you'll notice it as fewer surprises.
New Contributors
**Found something? Please open an issue: https://github.com/KartoffelToby/better_thermostat/issues
Full commit-level changelog
What's Changed
- Fix fahrenheit/celsius mishandling by @KartoffelToby in #2035
- test: validate bundled blueprints save with default inputs by @cl445 in #2047
- fix: remove dead calibration documentation links from config flow (#2040) by @cl445 in #2046
- fix: read TRV temperatures in the Home Assistant system unit by @cl445 in #2043
- fix: keep outdoor threshold working when recorder history is empty (#2038) by @cl445 in #2044
- feat: re-evaluate outdoor threshold on outdoor sensor changes by @cl445 in #2045
- chore: add diagnostic logging for TRV setpoint sync edge cases by @cl445 in #2048
- test: pin controller state-threading contract by @cl445 in #2050
- test: integration harness — real config entry against a simulated TRV by @cl445 in #2059
- docs: serve the documentation pages under their declared URLs by @cl445 in #2052
- Synchronize translations with current config flow, services and entities by @cl445 in #2062
- fix: sensor handling, store loading, and window-state hardening by @cl445 in #2054
- docs: keep README version info from going stale by @cl445 in #2064
- Disable current Preset of target temp changes by @KartoffelToby in #2067
- refactor: StateManager is the only owner of learned controller state by @cl445 in #2058
- build: manage the project with uv by @cl445 in #2027
- docs: remove stale content, fix broken doc images and 404 learn-more links by @cl445 in #2065
- refactor: typed Trv domain object replaces the real_trvs dicts by @cl445 in #2056
- mpc: seed fresh target-temp buckets from nearest sibling by @cl445 in #2002
- fix: controller state poison resistance (NaN/Inf healing at compute and restore time) by @cl445 in #2057
- build: type-check the component, strict on the calibration modules by @cl445 in #2073
- build: run pyrefly type checking in CI by @cl445 in #2075
- build: extend strict pyrefly to telemetry, retry and the Trv object by @cl445 in #2077
- docs: numpy-style docstrings for the model-fix quirk hooks by @cl445 in #2078
- chore(lint): enforce numpy docstring arg coverage and relax tests by @cl445 in #2083
- fix: allow removing the cooler entity from the configuration by @cl445 in #2091
- refactor: extract shared find_device_entity helper by @cl445 in #2101
- chore: translate remaining German comments to English by @cl445 in #2102
- fix: correct inital_tweak typo to initial_tweak by @cl445 in #2100
- chore: remove dead anti-flicker code from temperature event handler by @cl445 in #2099
- chore: trivial hygiene fixes (docstring, dead comment, boolean) by @cl445 in #2098
- fix: retry critical entities at startup before raising repair issue by @cl445 in #2093
- docs: added an algorithm selection guide to the README by @nizarbadriali in #2071
- Fix boot missing devices errors, AC Config flow removal, devel… by @KartoffelToby in #2095
- fix: compare group member setpoints in Celsius in group_all_members_off by @cl445 in #2107
- chore: drop stale open-caps mention from the reset_pid_learnings description by @cl445 in #2110
- fix: offer device automations for the Better Thermostat climate entity by @cl445 in #2112
- fix: stop the startup retry loop when the entity is removed by @cl445 in #2109
- chore: improve source comments to describe current behavior by @cl445 in #2096
- fix: recheck critical entities once the startup grace window elapses by @cl445 in #2108
- Fix dual-setpoint detection and TRV temperature handling bugs by @leow149 in #2105
- fix: harden the quirk set_temperature implementations activated by #2105 by @cl445 in #2111
- fix: only consider writable domains for the local calibration entity by @cl445 in #2113
- fix: confirm HVAC mode changes against the live TRV state by @cl445 in #2115
- fix: match setpoints with a tolerance across rounding grids by @cl445 in #2114
- feat: expose PID calibration diagnostics as sensors by @cl445 in #2097
- fix: register valve maintenance tick independently of calibration mode by @cl445 in #2127
- feat: add door sensor support with independent open/close delays by @cl445 in #2116
- test(benchmark): simulation core & scoring engine by @cl445 in #2087
- feat: link device relations and events in activity log by @KartoffelToby in #2129
- test(benchmark): restore the stacked benchmark suite stranded off develop (#2088, #2089) by @cl445 in #2132
- feat: MPC v2 calibration mode (QP + Kalman) — recovered onto develop by @cl445 in #2131
- Add cooling preset temperatures for HEAT_COOL mode by @cl445 in #2130
- fix(controlling): add send-cache and nil-guard to control_cooler() by @cl445 in #2133
- fix: render per-plant matrices for '--plant all' in benchmark runner by @cl445 in #2136
- fix: address remaining review findings from #2070 by @cl445 in #2135
- fix: don't hard-require daqp so BT installs on aarch64 (Raspberry Pi) by @cl445 in #2137
- feat: Z-Wave JS adapter + Eurotronic Spirit/Aeotec ZWA021 valve control by @cl445 in #2134
- fix: backport PID integration-step bound and cooler Fahrenheit dedup from v2 by @cl445 in #2139
- chore: update dependencies to the latest compatible versions by @cl445 in #2141
- fix(cooler): adopt only user setpoints from the cooler entity by @cl445 in #2145
- fix(cooler): write the payload a range-only cooler accepts by @cl445 in #2146
- refactor(events): share one inbound boundary for reported setpoints by @cl445 in #2147
- fix(cooler): survive an unknown cool target on a range-only cooler by @cl445 in #2154
- fix(climate): give every child its own setpoint step by @cl445 in #2155
- fix(degraded-mode): watch the door sensor and correct the repair text by @cl445 in #2156
- fix(cooler): delay the cooling switch-on by the tolerance by @cl445 in #2162
- fix(climate): bound an inbound setpoint instead of moving the other target by @cl445 in #2163
- fix(cooler): seed an unknown cool target from the cooler's own setpoint by @cl445 in #2164
- fix(cooler): decline a setpoint reported on a dead cooler state by @cl445 in #2179
- chore(helpers): read a published setpoint step through one shared rule by @cl445 in #2184
- fix(setpoint): match an inbound setpoint against the report and the clamp by @cl445 in #2190
- fix(cooler): bound re-injected targets and order the restored pair by @cl445 in #2177
- fix(mpc_v2): tie restored _initialised to the snapshot that seeded it by @cl445 in #2181
- docs: point the schedule guide at the preset actions by @cl445 in #2196
- fix: resolve a device-less thermostat model in the options flow by @cl445 in #2194
- fix: stop sending HVAC modes the TRV does not offer by @cl445 in #2198
- fix: re-assert an unconfirmed local calibration offset by @cl445 in #2201
- fix(cooler): surface an unavailable cooler through degraded mode by @cl445 in #2186
- chore(cooler): converge cooler-path wording and step tests across lines by @cl445 in #2188
- fix(calibration): make the cooling report and the idle gates agree by @cl445 in #2192
- fix: give a dual-role climate entity a single controlling channel by @cl445 in #2202
- fix: exercise TRV valves while the thermostat is off by @cl445 in #2208
- fix: agree on one default calibration mode by @cl445 in #2210
- docs: add a preset reference and a central-thermostat guide by @cl445 in #2212
- fix: keep the MQTT valve write inside the entity's declared bounds by @cl445 in #2218
- test: cover the quirk that runs for every device without one by @cl445 in #2220
- test: pin what a quirk module may define, and what it must by @cl445 in #2222
- Localize config-flow selectors and entity names by @cl445 in #2223
- fix(blueprints): stop weekly_heating_schedule from failing to save by @proscar87 in #2170
- fix(trvzb): land the requested valve position after a de-sticking bump by @cl445 in #2226
Full Changelog: 1.8.2...1.9.0-RC