Skip to content

1.9.0

Choose a tag to compare

@KartoffelToby KartoffelToby released this 21 Aug 18:22
· 309 commits to master since this release
5b4496d

About this release

With 1.9, we've shipped our winter 2026 release.

While release 2.0 will focus on cooling improvements for next summer, 1.9 already brings major upgrades: cooling now works just like heating, door sensors join window sensors, Z-Wave thermostats are supported, there's a new control algorithm to try, and a long list of small annoyances has been resolved.

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 and @wtom.


❄️ 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