Every heat pump ships with an answer to the same question: given the weather outside, how hot should the water going to the radiators be? The answer is a line on a graph — cold outside means hot water, warm outside means cool water — and the slope of that line, the heat curve. The factory default, in almost every case, is wrong for your house. Not badly wrong, but wrong enough that the gap between a default heat curve and a correctly-tuned one is the difference between a heat pump that performs well on paper and one that actually delivers the numbers in the brochure.
Worse, the default heat curve is also myopic. It lives in the moment. It doesn’t anticipate. It knows the current outdoor temperature, and to some extent the indoor temperature, and that is it. It doesn’t know the sun is, or will be, streaming in through the south windows, or that the wind is, or will be, pulling heat out faster than usual. It sets a flow temperature, commits to it, and waits for the next weather-driven update.
For a house like ours — 1874 stone, high thermal mass, 17 radiators, variable impact of wind speed and direction — a myopic heat curve is a bit like driving by looking only at the speed limit sign, never at the road. It gets you roughly where you’re going. But it leaves a lot on the table.
The thought experiment
The right control system, roughly, should think like a person who actually understands the house they live in — not the one in a warehouse where the heat pump was tested:
“It’s 3°C outside, and the forecast says it will stay around 3°C for the next few hours. The house will need about 6 kW of heat to hold 20°C. At the current flow rate, that means 32°C flow temperature. But the ground floor is already 20.4°C — we’re slightly ahead. Trim a degree off. And by the way, the wind drops tonight and turns south-westerly — the heat curve slope can ease back for the next few hours.”
No heat pump controller does this out of the box. They can’t, really — despite having access to the room temperatures, they don’t have the weather forecast, they don’t know the house’s heat loss coefficient, they don’t know what the wind is doing or how that will affect the fabric of our house. The information they’d need to reason like this lives in the monitoring stack, not in the heat pump.
So we built it there.
Layer one: the heat curve, but moving
The Vaillant’s own logic is, at heart, a single equation relating flow temperature to outdoor temperature and a heat curve slope. Reverse-engineering what the controller actually does (thanks to the work of Andre_K), the relationship comes out as:
TFlow = 2.55 × (HC × (Tset − Tout))^0.78 + Tset
Where Tset is the indoor setpoint, Tout is outdoor temperature, and HC (Heat Curve) is the heat curve slope. The exponent of 0.78 is the non-linearity — radiators output heat as roughly a 1.3-power function of their temperature-above-room, so the inverse relationship lives at that exponent inverted.
A textbook control theorist would probably use the equation to back-solve: start from the heat loss coefficient, compute the house’s current demand from the indoor–outdoor differential, work out what flow temperature is needed to deliver it, and then invert the formula to find the HC value that produces that flow temperature. Clean, principled, reproducible. I considered it. I didn’t do it.
What the coarse layer actually does is more pragmatic. The base HC is a piecewise-linear lookup against outdoor temperature — a hand-tuned curve, anchored at 0.75 for −3°C and colder, sloping down to 0.40 above 16°C, with a gentle inflection around 10–12°C where the house’s response changes character. That curve was tuned empirically against the house’s actual behaviour. Which means our measured heat loss coefficient (HLC) of 0.540 kW/K is baked into it rather than computed from it. The HLC sits in the coefficients, not in the code path.
The coarse slope is derived from this lookup and nothing else. No forecast term, no indoor feedback, no wind correction, no small-weather boost — all of those live in the fine layer, for reasons that will become clear in a moment. The coarse layer’s one job is to track the outdoor-temperature regime.
That slope gets snapped to the nearest 0.05 on the HC dial — the only granularity the Vaillant’s own register understands — clamped to the 0.20–0.95 range, and, as a matter of discipline, only written to the heat pump if the change exceeds 0.045. In practice that means the slope only moves when outdoor temperature has crossed a regime boundary. Writing tiny adjustments every few minutes isn’t control; it’s fidgeting.
That threshold matters. The heat pump’s internal state machine responds to curve changes by re-evaluating its compressor behaviour, and constantly nudging the slope sends it into small cycles of confusion. Let it settle. Only nudge when outdoor conditions genuinely cross a regime boundary.
Layer two: the trim
The coarse layer is good but slow. It captures sustained changes — the cold snap that lasts 48 hours, the shoulder-season warmth that drifts in over a week. It doesn’t capture the thing happening right now.
That’s what the fine layer is for.
Every cycle, four small adjustments are computed against current conditions and summed. A mild boost in the −1 to 9°C band, where observation showed the base curve was slightly conservative. A forecast-aware correction that looks six hours ahead at OpenMeteo, weights the nearer hours more heavily, and if the forecast diverges sharply from current conditions, blends the two rather than trusting either alone. An indoor-temperature feedback term — the average of five ground-floor sensors — that trims up or down if the house is drifting from setpoint. And a wind-exposure boost that only kicks in when the wind is coming from the 0–110° arc — the direction the house is most exposed to.
The four are summed in heat-curve-equivalent units, then converted to a °C trim by a sensitivity factor — how much flow temperature each unit of heat curve change actually buys you. That °C value is written to a Vaillant register called Hc1ExcessTemp: a trim that sits on top of whatever target flow temperature the coarse curve is producing, added or subtracted in small increments.
The sensitivity factor comes from the Vaillant formula above, differentiated with respect to the heat curve:
∂TFlow / ∂HC = 2.55 × 0.78 × (Tset − Tout)^0.78 × HC^−0.22
Which, in plain English, says: the colder it is outside, the more flow temperature you get from each unit of heat curve change. Trim harder in winter, more gently in shoulder seasons. The formula does this for us automatically — and it applies uniformly to all four adjustments, so the wind boost on a cold day gets scaled up by exactly the same factor as the indoor feedback.
There is a quiet architectural choice hiding in this. All four adjustments could have been stacked onto the coarse slope instead — it’s the obvious thing to do. They aren’t, for two reasons. First, the slope is quantised to 0.05 steps, so small continuous adjustments would round away and be lost. Second, the Vaillant flinches every time the slope is rewritten, re-evaluating its compressor behaviour; a trim value can be updated every cycle without disturbing it. So the slope gets to do what slopes do — track regime — and the trim does what trims do: reject disturbance. Two layers, two jobs, no overlap.
Coarse adjustment plus fine trim, operating on different timescales and with a clean separation of what each is responsible for, is exactly the structure every good control system in any domain eventually converges on — from aircraft autopilots to thermostats to central banks. Slow loops for steady state. Fast loops for disturbance rejection. Let each do what it’s good at.
The hydraulic body around the brain
None of this works if the plumbing isn’t cooperating. A heat pump that thinks is only as good as the pipes that carry its decisions to the radiators, and the 17 smart TRVs on those radiators are each, in effect, small autonomous controllers of their own. Left to their defaults, they would close when their rooms get warm, starving the flow loop and confusing the heat pump into thinking the house is satisfied when it isn’t.
The smart TRVs are managed in Home Assistant through an integration called Versatile Thermostat, which uses a TPI algorithm to modulate each valve’s opening percentage. Every TRV has a maximum closing setting so that it never shuts fully, guaranteeing a design flow rate of around 2,014 L/h through the system at all times. That alone isn’t quite enough — as the TRVs open and close to match each room’s heat demand, the system flow rate still wanders. So one TRV — the utility room’s, because nobody lives there — is repurposed as a compensating valve: an automation trims it open or closed in real time to hold the whole-system flow rate at target, absorbing the small variations as the other 16 TRVs move around it.
Each TRV takes its cue from the room temperature of the space it serves, and the lockshield side of every radiator is left fully open — the smart TRVs are the balancing device. That went directly against the installer’s advice, which was to replace every smart TRV with a lockshield. One more piece of received wisdom that didn’t survive contact with the data.
The result is a hydraulic circuit that behaves, from the heat pump’s perspective, as one predictable load rather than 17 competing ones. That predictability is what allows the coarse–fine control architecture to work. Without it, the flow temperature sensitivity formula wouldn’t hold, and the trim loop would be chasing noise.
What it actually bought us
After the two-layer architecture went in, COP improved — not dramatically, because the low-hanging fruit was already picked, but consistently and measurably across outdoor temperatures. More importantly, the behaviour of the system became legible. We could look at a day’s flow temperature trace and say which adjustments were coarse-layer response to the weather and which were fine-layer response to solar gain, wind, or occupancy. The heat pump stopped being a thing that happened to the house and started being something the house was in conversation with.
That conversation is what turns a factory-default ASHP into one that delivers a seasonal COP of 4.21 in a solid-stone Victorian building and most importantly a comfortable and dry home. It’s not a hardware story. It’s a reasoning story — orchestrated inside Home Assistant, speaking to the Vaillant over a protocol it was never designed to be talked to over.
One thing I should be add before closing: I am not an engineer. Before this heat pump, I knew nothing about heating or controls. What’s above is the result of a lot of reading, a lot of iteration, and using AI tools like Claude, which I’d never used before any of this started. There were wrong turns and wasted evenings. There were moments the formula looked right on paper and behaved badly on the house. There was a lot of trying things, watching what happened, and going back.
A heat pump that thinks for itself, in the end, is just a heat pump given enough information to reason with. The information was always there. Somebody had to go and fetch it — and the tools to do that are now, finally, within reach of anyone willing to look.