Underfloor Heating Controller

The first Home Assistant integration purpose-built for hydronic underfloor heating systems.

While existing thermostats adapt radiator/TRV logic to UFH, this integration addresses UFH’s unique requirements: high thermal mass, slow response times, multi-zone coordination through shared heat sources, and valve scheduling to prevent rapid cycling.

Highlights

PID-Based Temperature Control

  • Tuned for concrete screed thermal response
  • Per-zone PID parameters (Kp, Ki, Kd) with anti-windup protection
  • EMA smoothing handles noisy wireless sensors (Zigbee, WiFi)

Multi-Zone Coordination

  • Zones aggregate demands for efficient boiler firing
  • Observation period scheduling (2-hour windows) prevents rapid valve cycling
  • Quota-based valve management allocates time based on PID duty cycle

Native Boiler Integration

  • Smart heat request signaling - waits for valves to fully open before firing
  • DHW priority handling - blocks new heating during hot water
  • Latent heat capture - flush circuits capture residual boiler heat after DHW
  • Summer mode control - enables/disables heating circuit based on demand
  • Compatible with Bosch, Buderus, Nefit, Junkers, Worcester via EMS-ESP

Zone Fault Isolation

  • Independent zone failures - one failing sensor doesn’t affect other zones
  • Graceful degradation - failed zones use last-known demand for 1 hour before fail-safe
  • Safe initialization - no valve actions until all zones have valid readings
  • Automatic recovery when sensors reconnect

More details in the GitHub release

See also the documentation

3 Likes

Hi,

Does this controller also account for hybrid systems with some rooms in the house being heated by radiators instead of UFH and thus requiring a different or possibly higher temperature of the boiler supply temp?

This integration does not control boiler supply temperatures, that falls outside the scope here. But in general, the control logic can be used for radiators if you wish.

Amazing project, thank you for sharing. I am testing this right now with one room.
Are you planning to implement more advanced optimization of PID paraameters based on historic performance? I am not sure how to manually optimize PID parameters to get to optimized results.

What’s Changed in v0.2.0 since v0.1.0

  • Hide zone analysis entities by default
  • Set flush request timing to activate only after DHW ends
  • Centralize version management in const module
  • Fix observation period alignment to respect configured value
  • Add force-update mechanism for external dead-man-switch support
  • Watch controller entities for external state changes
  • Add Tasmota relay configuration documentation
  • Add valve unavailability as degraded/fail-safe trigger
  • Add shorter initializing timeout for faster fail-safe reporting
  • Remove circulation_entity configuration option

Full Changelog: Comparing v0.1.0...v0.2.0 · lnagel/hass-ufh-controller · GitHub

I’m planning to add heat accounting based on supply temperature. More details and opportunity to comment in GitHub. Any comments and challenges welcome.

v0.3.0 released

New Features

  • Heating curve for outdoor temperature compensation (#76) — Dynamically calculates supply target temperature based on outdoor conditions using two-point linear interpolation. When an outdoor temperature sensor is configured, the system adjusts the supply target between warm and cold design points instead of using a fixed value. Includes a new controller-level supply target sensor entity.
  • Heat accounting with supply temperature normalization (#64) — Zones now accumulate quota weighted by actual heat delivery via a supply coefficient. When supply temperature is below target, zones consume quota slower and stay open longer. Includes a new supply coefficient sensor per zone and a flow binary sensor per zone. Configurable via a new “Heat Accounting” options flow step.
  • Quickstart guide (#68) — New task-oriented guide (docs/quickstart.md) walking users from installation to a working first zone.

Bug Fixes

  • Fix crash on config entry unload after in-place config reload (#65) — Stale listener callbacks caused ValueError: list.remove(x): x not in list on entry unload. Fixed by adding a dedicated shutdown() method registered once during setup.
  • Fix flaky test due to EMA floating-point precision (#81, #86) — Use pytest.approx() for float assertions where values pass through EMA smoothing.

Improvements

  • PID error icons changed to plus/minus (#66) — Replaced chevron icons (too similar to check icon) with mdi:thermometer-plus (cold) and mdi:thermometer-minus (warm).
  • Zone diagnostic entities hidden from dashboards — Zone sensors and binary sensors now have entity_registry_visible_default = False. Mode select and flush switch remain visible.
  • Entity recreation on config change — Changing supply temp, outdoor temp, or DHW entity in config now triggers a
    full reload to recreate affected entities.

Refactoring / Internal

  • Storage format V2 (#78) — Coordinator state restructured with controller data nested under a controller key. PID keys renamed for consistency. Includes automatic V1 → V2 migration.
  • Entity declarations refactored to description-based patterns (#77) — All entity metadata and behavior defined in entity descriptions with generic entity classes using callbacks.
  • PIDState fields renamed (#82) — p_termproportional, i_termintegral, d_termderivative.
  • TimingParams renamed to TimingConfig throughout codebase.
  • Test reorganization (#63) — Moved pure controller/zone tests from integration/ to unit/.

Full Changelog: Comparing v0.2.1...v0.3.0 · lnagel/hass-ufh-controller · GitHub

v0.4.0 + Physics-Based Simulation Framework

Two updates to share — v0.4.0 focused on cleaning up the internals and improving diagnostic visibility, and a simulation framework that landed right after to support upcoming control algorithm work.

What changed in v0.4.0

The main user-facing additions are better diagnostic sensors. The controller now exposes zone counting sensors (zones_flowing, zones_heating, zones_window) so you can see at a glance how many zones are in each state. The heat request signal moved from per-zone tracking to a single controller-level binary sensor, which better reflects how the system actually works — one aggregated signal to the boiler.

Each zone also gets a new heat state (flow confirmed AND supply coefficient above 10%) and a remaining duration sensor showing how much time is left in the current valve run. Useful for dashboards and for understanding why a zone just turned off.

On the reliability side, the controller now defers initialization until all watched entities (valves, DHW sensor, supply/outdoor temp sensors) have reported valid state. This prevents the controller from issuing commands to entities that haven’t loaded yet after an HA restart — something that could cause brief spurious valve actions on slower systems. After 120 seconds it proceeds anyway and logs a warning, so a dead sensor won’t block startup forever.

Breaking change: Several entity IDs were renamed for consistency. If you have automations or dashboard cards referencing zone-level heat_request or blocked entities, you’ll need to update them — see the full changelog for the mapping.

Physics-based simulation framework

This is the part I’m most excited about. The integration now includes a thermal simulation test suite that runs the real HeatingController code — the exact same code running in production — against a lumped-capacitance room model. No HA dependencies, no mocks of the control logic itself. Each simulated room computes heat gain from the floor circuit minus envelope losses at 60-second timesteps, with a realistic valve actuator model (3-minute open ramp, 85% threshold before heat delivery).

Four room archetypes cover the range from Passivhaus (~60h thermal time constant) to pre-1960s uninsulated buildings (~13h), with parameters derived from EN 12831, ISO 13790, and EN 1264. The test suite validates:

  • Steady-state convergence — does the controller actually reach and hold the setpoint across different building types and outdoor temperatures?
  • Anti-windup behavior — does the integral term clamp correctly when a setpoint is physically unreachable?
  • Disturbance recovery — window openings, setpoint changes, and outdoor temperature drops mid-simulation
  • Multi-zone fairness — do zones with different demands get proportional heating time without starving each other?
  • Borderline duty cycles — stable behavior at the minimum run time threshold where the valve might skip periods entirely

The full suite of 47 simulation tests runs in about two seconds. Seven tests are intentionally marked as expected failures to document genuine controller limitations (cold-start overshoot from integral windup, oscillation in leaky rooms outside the UFH design envelope) without breaking CI.

Why this matters for what’s coming next

@mroggi asked about PID parameter optimization earlier — the simulation framework is the foundation for that work. It lets me verify control algorithm changes against realistic thermal conditions before they reach anyone’s house.

The immediate next step is supply flow temperature limits — letting you define upper and lower bounds to keep the boiler operating in its efficient condensing range. But that feature depends on integral back-calculation corrections so the PID doesn’t wind up when flow constraints override what the controller wants to do. And verifying that back-calculation behavior across different building types and operating conditions is exactly what the simulation framework was built for.

So the sequence was: build the simulation harness → verify current behavior → safely implement constrained-output PID → expose flow limits to users. I’d rather take this in the right order than ship a feature that causes overshoot in edge cases.

If you’re curious about the simulation details, there’s a full write-up in the docs.


Install via HACS (custom repository): https://github.com/lnagel/hass-ufh-controller
Docs: Full documentation index

3 Likes

Great stuff! and great timing, I will feedback in a few weeks once I’ve given it a closer look and tried it. Will be installing end Feb start of March.

Hi - this looks very interesting to me. I'm just in the process of the 2nd stage of modernising the heating control of my 14-zone UFH system in my old (badly insulated) house. It was installed nearly 20 years ago with simple on/off thermostats per zone and timer switch for each block of 7 zones (top 2 floors, bottom 2 floors of the house), where each block has a UFH pump.

In my system there is a thermal store which is used both directly for UFH and (via a heat-exchanger) indirectly for DHW. The boiler runs off a thermostat (again with timer) attached to the thermal store - so the boiler fires up, efficiently fills the store with hot water, and then the UFH and/or DHW pull heat from that over the 30-60 minutes, say, before the temperature has dropped enough that the boiler fires up again. I don't currently have control over boiler output temperature (old boiler!) although I'm working on that.

I'm interested to test what you've built here, but my setup seems slightly different, in that I have the buffer of the thermal store in between the boiler and the users of heat (UFH/DHW).

Any thoughts as to whether I can effectively use this?

I believe you can use it. It would work well if you've got a supply temperature sensor for the UFH manifold supply water, which would allow for the valve run times to be accounted proportionally. See the docs in the repo for further details.

Thank you - I'll be testing this out. I'm just in the process of getting EMS-ESP setup to control my old boiler with more flexibility (i.e. to be able to modulate the temperature, instead of it being fixed).

If I understand correctly, your controller will be able to cleverly control the boiler via EMS-ESP so that (a) the boiler runs at a low temperature most of the time - e.g. 30-40 degrees suited to UFH, (b) the boiler only runs at very high temperature when scheduled for Hot Water/Showers/etc. Is that right?

1 Like