Skip to content

Add cooling / ice-bath support (heat *and* cool) - #5

Open
thorwaler wants to merge 2 commits into
lweberru:mainfrom
thorwaler:main
Open

Add cooling / ice-bath support (heat *and* cool)#5
thorwaler wants to merge 2 commits into
lweberru:mainfrom
thorwaler:main

Conversation

@thorwaler

Copy link
Copy Markdown

Why

The controller is currently heating-only: it demands heat while water is below
target and treats water >= target as "satisfied". For ice baths / cold plunges the
goal is inverted — run the pump + aux output (a chiller) to cool down to a target
that sits below the current water temperature. Today the controller simply does
nothing when the water is too warm.

This PR makes heating/cooling a selectable, persisted hvac_mode on the climate
entity (heat / cool / off) and inverts the thermostat logic in cool.

What changed

All temperature decisions flow through one hysteresis helper plus two "target
reached" guards, so the change is small and localised.

const.py

  • OPT_KEY_COOL_MODE = "cool_mode", DEFAULT_COOL_MODE = False.

coordinator.py

  • Read self.cool_mode from options in __init__ (next to hvac_enabled).
  • New async def set_cool_mode(self, cool) that persists to options, mirroring
    set_hvac_enabled.
  • _thermostat_demand() is now direction-aware:
if getattr(self, "cool_mode", False):
    # Cooling: ON above (target+hot), OFF at/below (target-cold)
    if prev_on:
        return ct > (tt - cold)
    return ct > (tt + hot)
# Heating (unchanged)
if prev_on:
    return ct < (tt + hot)
return ct < (tt - cold)
  • The two water_temp >= target_temp_effective → demand off guards (PV path and aux
    path) are direction-aware — in cool mode "reached" is water <= target.

climate.py

  • Add HVACMode.COOL to _attr_hvac_modes.
  • hvac_mode returns COOL when cool_mode is set; hvac_action returns
    HVACAction.COOLING when active in cool mode.
  • async_set_hvac_mode: COOLset_cool_mode(True) + enable; HEAT
    set_cool_mode(False) + enable; OFF → disable.

sensor.py (minor, separate commit — drop if unwanted)

  • Change the hardcoded chlor_spoons unit from "Löffel" to "spoons" so the dose
    suggestion reads in English.

Behaviour

  • cool runs the pump + aux (chiller) when water is above target and stops with the
    same hysteresis once at/below it.
  • PV / power-saving demand inverts cleanly too, so "cool on solar surplus" works.
  • Heating behaviour is unchanged when cool_mode is off (the default).

One decision to confirm

I removed the start_manual_heat_to_target() call from the HEAT branch of
async_set_hvac_mode. As-is, selecting heat starts a bathing timer (manual
heat-to-target), which is surprising for a plain heat/cool toggle — it turned bathing
mode on every time I switched to heat. Removing it makes heat/cool symmetric
(continuous thermostat control). If you'd prefer to keep the manual-heat behaviour on
heat, happy to gate it so only the toggle path skips it.

Deliberately left heating-tuned (possible follow-ups)

  • Frost protection still forces pump/main near freezing — a useful floor for ice
    baths, but it overrides cooling below the frost threshold.
  • Boost and dynamic-target season/weather offsets remain heating-shaped.

Testing

With cool selected and target a few degrees below water temp: the aux (chiller)
switch fires, hvac_action reports cooling, and it cuts out once water drops past
target with the configured hysteresis. Heating is unchanged with cool_mode off.

andreas added 2 commits August 6, 2026 20:26
Replaces the static cool_mode option with a proper hvac_mode toggle on the
climate entity, so heat vs cool can be switched from the frontend (any
thermostat/climate card) or an automation, and is persisted.

- climate: expose HVACMode.COOL; hvac_mode returns cool when active;
  hvac_action reports COOLING; async_set_hvac_mode(COOL/HEAT) flips the mode
  via the coordinator.
- coordinator: cool_mode state read from + persisted to options; new
  set_cool_mode(); direction-aware _thermostat_demand and the two
  'target reached' guards.
- const: OPT_KEY_COOL_MODE / DEFAULT_COOL_MODE.

Setting the climate to 'cool' regulates temperature downward (runs pump/aux
when water is ABOVE target); 'heat' is unchanged; persists across restarts.
Changes the hardcoded chlor_spoons unit_of_measurement from German 'Löffel' to 'spoons' so the dosing suggestion reads in English.
@thorwaler

Copy link
Copy Markdown
Author

Companion frontend PR: lweberru/pool_controller_dashboard_frontend#2 — adds the matching cooling display + heat/cool toggle to the card.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants