Three devices, one room, no shared rule
A room with underfloor heating and an air conditioner has two ways to change its temperature, and a humidifier gives it a third path to how the room feels. Wire each to its own thermostat and they will work against each other.
The usual failure is undramatic and expensive: the floor loop heats up to the point where the air conditioner starts cooling, and the two run against one another all evening. Nothing alarms, nothing breaks, the room sits at roughly the right temperature, and the energy bill quietly doubles.
The fix is not a better thermostat. It is one rule that owns every output in the zone.
The dead band, and the mistake inside it
Each zone gets a dead band around the setpoint — here ±0.5 °C. Below it, heat. Above it, cool. Inside it, do nothing.
That last branch is where implementations go wrong. It is tempting to hold the previous state inside the band, on the grounds that the room is close enough. Holding is exactly what lets the heating loop keep running until the cooling threshold is crossed. The band exists to protect compressors and valve drives from short cycling, not to keep equipment running.
if (temperature < setpoint - hysteresis) {
write(cfg.heater, true);
if (hasCooler) write(cfg.cooler, false);
} else if (temperature > setpoint + hysteresis) {
write(cfg.heater, false);
if (hasCooler) write(cfg.cooler, coolingAllowed);
} else {
// Inside the dead band - release both outputs.
write(cfg.heater, false);
if (hasCooler) write(cfg.cooler, false);
}A season flag, so a warm afternoon cannot start the cooling
A house in heating mode should not run an air conditioner because one south-facing room warmed up after lunch. A single shared season flag locks cooling out across every zone, and the zone rule reads it before it is allowed to call for cooling. One control, one place to change it.
One sensor describes one point on one wall
A control loop is only as good as the number it is given. A single sensor reports the temperature where it happens to be mounted. In a room with a glazed facade, a warm floor and a draught path, other points in that same room differ by several degrees — so the loop chases a value that describes almost nothing.
The zone therefore acts on an aggregate of several sensors, published as one value. But averaging alone has a hole in it: a sensor stuck at a plausible 22 °C does not look broken, and averaging simply spreads its error across the zone. Comparing each reading against the median of the group catches the stuck one instead.
var reference = median(readings);
used = readings.filter(function (value) {
return Math.abs(value - reference) <= cfg.maxDeviation;
});
// Below three readings there is no majority to compare against,
// so the check stands down rather than guessing.
if (used.length === 0) used = readings;What to do when the number is missing
A failed sensor usually reports null or an empty string. Dropping it and running on whatever is left keeps the zone alive. If nothing valid is left at all, the outputs hold their last state and the rule logs a warning.
Holding is the right default here. A zone that keeps its last state for an hour is a comfort problem; a zone that acts on a garbage reading is a burst pipe or a compressor running against a closed valve.
Checklist for a climate zone
- One rule owns every output in the zone — heating, cooling, and anything that changes how the room feels.
- The dead band releases both outputs, it does not hold the previous state.
- Cooling is locked out by a season flag, shared by all zones.
- The zone temperature is an aggregate, not one sensor.
- Outliers are rejected against the median, not averaged in.
- An invalid reading holds the outputs and raises a warning — it never drives them.
- Adding a zone is a block of configuration, not a copy of the program.
See it running
This logic runs on a 350 m² house across seven zones. The published address export shows every setpoint, current temperature and heating output the rules talk to — searchable, and downloadable as CSV.
The 350 m² climate case, with the full address exportKNX programming as a service