Cold Dip Automation

Cold Dip Automation: Saving My Marriage One Sensor at a Time

Original GitHub Post

HomeAutomation/ColdDip/README.md at main · shaybyrne-work/HomeAutomation

The Backstory

Over a year ago, my wife decided that casually plunging into frozen lakes in Muskoka, Ontario just wasn’t hardcore enough. Nope — she wanted to bring that delightful near-death experience right to our doorstep.

Before I could even muster a, “Wait, what?!”, there it was — her very own “cold plunge pool” proudly occupying prime real estate on our back deck.

I say “pool,” but let’s call it what it really was — a livestock water trough. That’s right. The kind designed for cows, not spa enthusiasts. Classy!!

The Carpenter’s Dilemma

First and foremost, being a carpenter, seeing that eyesore sitting on my deck was unacceptable. So, like any good husband, I over-engineered the hell out of it.

:boom: BOOM! Enter the Enhancements:

  • :white_check_mark: 2” rigid insulation box
  • :white_check_mark: Padded insulated lid
  • :white_check_mark: Small heater pad
  • :white_check_mark: Charcoal water filter and air pump
  • :white_check_mark: Smart plug automation

Oh LA LA… Very Spa Like!!

The First Attempt: A Noble but Flawed Effort

Step 1: Insulation

Built a custom box around the tank using 2” rigid insulation and a padded lid — because even ice baths deserve a little luxury.

Step 2: Heating

Slapped a small heater pad to the side, hoping it would fend off the frost.

Step 3: Filtration

Added a charcoal filter pump and air pump — because my wife draws the line at murky water.

Step 4: Automation

Tied it all together with a smart plug and a basic Home Assistant automation that turned it on whenever the outdoor temperature dipped below freezing.

It was simple. It was elegant. It was… not enough.

Extra Sensor - Shelly Uni + DS18B20

I added a Shelly Uni with a DS18B20 temperature sensor to monitor water temperature. It still wasn’t enough.


The Harsh Reality of Canadian Winters

When it hits -20°C (-4°F), no tiny heater pad stands a chance.

:rotating_light: The Results?

  • :snowflake: Ice formed anyway.
  • :muscle: My wife had to break the ice to get in.
  • :rage: My Shelly Uni constantly disconnected.
  • :triumph: My frustration level hit record highs.

Round Two: The Ultimate Cold Dip Upgrade

I officially declared Phase 1 a “prototype” — a fancy way of saying it failed, but at least I learned something.

Luckily, my wife accepted the first-year learning curve… but immediately tried to convince me that buying one of those fancy hipster cold plunges wasn’t that bad of an idea.

Nice try. But now it was personal.

Winter 2024/2025 was fast approaching, and I had one goal:
Build the most over-the-top, sensor-packed, Home Assistant-powered cold plunge Ontario has ever seen.


The Upgrades

Clearly, More Heat + Better Insulation Was Needed

  • :small_blue_diamond: Two 120V RV tank heating pads
  • :small_blue_diamond: NASA-grade Bubble-Pack Insulation
  • :small_blue_diamond: Shelly Plus 2PM (for heating pad control)
  • :small_blue_diamond: Shelly Plus Add-On + DS18B20 Temperature Sensor

Bonus Features

  • Separate Cleaning Cycle: The charcoal filter and air pump were split onto their own smart plug, handled by a separate cleaning automation.
  • Automation logic: Refined to adapt to environmental conditions instead of blindly running on/off.

Home Assistant: Where the Real Magic Happens

If you’ve ever fallen into the Home Assistant rabbit hole, you know there’s no such thing as done. There’s always something to tweak.

This was no longer about keeping the water unfrozen — this was about making the most efficient, reliable, and hands-off system possible.


The Secret Sauce: Smarter Binary Sensor

DS18B20 sensors can be flappy (technical term). Water cools unevenly, so the sensor often bounced between on and off.

The Solution

:white_check_mark: A binary sensor template with hysteresis.
This prevents rapid cycling and gives the heater pads a fighting chance.

# Template binary sensor for cold dip below 2.9°C with hysteresis
- binary_sensor:
    - name: cold_dip_below_threshold
      unique_id: cold_dip_below_threshold
      state: >
        {% set temp = states('sensor.cold_dip_mats_and_temperature_shelly_temperature') | float %}
        {% if temp < 2.9 %}
          true
        {% elif states('binary_sensor.cold_dip_below_threshold') == 'on' and temp < 3.2 %}
          true
        {% else %}
          false
        {% endif %}

# Template binary sensor for cold dip above 3.4°C (reporting only)
- binary_sensor:
    - name: cold_dip_above_threshold
      unique_id: cold_dip_above_threshold
      state: >
        {{ states('sensor.cold_dip_mats_and_temperature_shelly_temperature') | float > 3.2 }}

Helpers & Automations

Without some failsafes, this could have quickly turned into an out-of-control automation nightmare. These helpers kept things sane:

:white_check_mark: Helpers

  • Cold Dip Automation Lock: Prevents overlapping runs.
  • Cold Dip Cleaning: Separates cleaning and heating.
  • Cold Dip Heat Mat Timer: Controls runtime so the pads don’t run 24/7.

:rocket: Automation: The Final Boss

The automation now reacts to:

  • :heavy_check_mark: Outdoor temperature forecasts — Longer heating at -20°C, shorter at -5°C.
  • :heavy_check_mark: Current pool temperature — Stops unnecessary heating.
  • :heavy_check_mark: Manual overrides — Because sometimes you just want a button.
  • :heavy_check_mark: Automation Lock (Mutex) — Ensures only one automation run at a time.

And just like that, my cold dip automation dreams became a cold, well-regulated reality.

alias: Cold Dip Heating Automation
description: Winter Automation to Manage Cold Dip Heat Mats
triggers:
  - entity_id:
      - binary_sensor.cold_dip_threshold
    id: Cold_Dip_Below_SetPoint
    trigger: state
    to: "on"
  - entity_id: switch.cold_dip_heater
    to: "on"
    id: Cold_Dip_Mats_Manual_On
    trigger: state
  - event_type: timer.finished
    event_data:
      entity_id: timer.cold_dip_timer
    id: Cold_Dip_Timer_Finished
    trigger: event
  - entity_id: switch.cold_dip_heater
    to: "off"
    id: Cold_Dip_Mats_Manual_Off
    trigger: state
  - trigger: time
    at: "00:00:00"
    id: Counter_Reset
conditions:
  - condition: template
    value_template: "{{ now().month in [11, 12, 1, 2, 3, 4] }}"
actions:
  - choose:
      - conditions:
          - condition: and
            conditions:
              - condition: trigger
                id: Cold_Dip_Below_SetPoint
              - condition: state
                entity_id: input_boolean.cold_dip_lock
                state: "off"
        sequence:
          - action: input_boolean.turn_on
            target:
              entity_id: input_boolean.cold_dip_lock
          - if:
              - condition: state
                entity_id: timer.cold_dip_cleaning
                state: active
            then:
              - action: timer.cancel
                target:
                  entity_id: timer.cold_dip_cleaning
          - condition: state
            entity_id: timer.cold_dip_timer
            state: idle
          - target:
              entity_id: switch.cold_dip_heater
            action: switch.turn_on
          - target:
              entity_id: switch.cold_dip_pump
            action: switch.turn_on
          - choose:
              - conditions:
                  - condition: numeric_state
                    entity_id: weather.local_forecast
                    attribute: temperature
                    below: -20
                sequence:
                  - target:
                      entity_id:
                        - timer.cold_dip_timer
                        - timer.cold_dip_cleaning
                    data:
                      duration: "04:00:00"
                    action: timer.start
              - conditions:
                  - condition: numeric_state
                    entity_id: weather.local_forecast
                    attribute: temperature
                    below: -10
                sequence:
                  - target:
                      entity_id:
                        - timer.cold_dip_timer
                        - timer.cold_dip_cleaning
                    data:
                      duration: "03:00:00"
                    action: timer.start
            default:
              - target:
                  entity_id:
                    - timer.cold_dip_timer
                    - timer.cold_dip_cleaning
                data:
                  duration: "02:00:00"
                action: timer.start
        alias: Cold_Dip_Below_SetPoint
      - conditions:
          - condition: trigger
            id: Cold_Dip_Mats_Manual_On
        sequence:
          - action: input_boolean.turn_on
            target:
              entity_id: input_boolean.cold_dip_lock
          - target:
              entity_id: switch.cold_dip_pump
            action: switch.turn_on
          - if:
              - condition: state
                entity_id: timer.cold_dip_cleaning
                state: active
            then:
              - action: timer.cancel
                target:
                  entity_id: timer.cold_dip_cleaning
          - choose:
              - conditions:
                  - condition: numeric_state
                    entity_id: weather.local_forecast
                    attribute: temperature
                    below: -20
                sequence:
                  - target:
                      entity_id:
                        - timer.cold_dip_timer
                        - timer.cold_dip_cleaning
                    data:
                      duration: "04:00:00"
                    action: timer.start
              - conditions:
                  - condition: numeric_state
                    entity_id: weather.local_forecast
                    attribute: temperature
                    below: -10
                sequence:
                  - target:
                      entity_id:
                        - timer.cold_dip_timer
                        - timer.cold_dip_cleaning
                    data:
                      duration: "03:00:00"
                    action: timer.start
            default:
              - target:
                  entity_id:
                    - timer.cold_dip_timer
                    - timer.cold_dip_cleaning
                data:
                  duration: "02:00:00"
                action: timer.start
        alias: Cold_Dip_Mats_Manual_On
      - conditions:
          - condition: trigger
            id: Cold_Dip_Timer_Finished
        sequence:
          - target:
              entity_id:
                - switch.cold_dip_heater
                - switch.cold_dip_pump
            action: switch.turn_off
          - target:
              entity_id:
                - timer.cold_dip_timer
                - timer.cold_dip_cleaning
            action: timer.finish
          - action: counter.increment
            target:
              entity_id: counter.cold_dip_runtime
          - action: input_boolean.turn_off
            target:
              entity_id: input_boolean.cold_dip_lock
        alias: Cold_Dip_Timer_Finished
      - conditions:
          - condition: trigger
            id: Cold_Dip_Mats_Manual_Off
        sequence:
          - target:
              entity_id:
                - switch.cold_dip_pump
                - switch.cold_dip_heater
            action: switch.turn_off
          - target:
              entity_id:
                - timer.cold_dip_timer
                - timer.cold_dip_cleaning
            action: timer.finish
          - action: counter.increment
            target:
              entity_id: counter.cold_dip_runtime
          - action: input_boolean.turn_off
            target:
              entity_id: input_boolean.cold_dip_lock
        alias: Cold_Dip_Mats_Manual_Off
      - conditions:
          - condition: trigger
            id: Counter_Reset
        sequence:
          - action: counter.reset
            target:
              entity_id: counter.cold_dip_runtime
        alias: Counter_Reset
mode: restart

Final Thoughts

This all kicked off in September 2023, with Phase 2 starting in September 2024. The system officially went live with a water fill on November 28, 2024, and after endless tweaks, cursing at YAML, and fighting through January hiccups, the system settled at a beautiful average temperature of 3.1°C.

:muscle: After All That:

  • :heavy_check_mark: The pool never froze this season.
  • :heavy_check_mark: My wife is thrilled (the only real success metric).
  • :heavy_check_mark: I’ve retired from my unpaid, frostbitten job as Chief Polar Plunge Engineer.

And the Best Part?

Not only did I automate a glorified cow trough into the Cadillac of backyard ice baths, I out-automated every overpriced, influencer-endorsed cold plunge on Instagram.

Sorry hipsters — my cattle trough has logs, sensors, and a personal grudge against paying $10k for fancy frozen water.

February 2025 and it’s still not frozen!

Would I Do It Again?

:white_check_mark: Absolutely.

Would I rather be soaking in my hot tub instead of freezing my hands off fixing sensors?
:white_check_mark: 1000%.

But hey — happy wife, happy life… even if it means automating a cattle trough full of ice water instead of enjoying a warm soak like a sane person.


:warning: Disclaimer

The YAML, automations, and configurations shared in this post have been edited for privacy and are provided purely for inspiration and entertainment.

I’m by no means a Home Assistant expert — I’m just a stubborn Irish guy living in Canada, trying to keep a glorified cattle trough from becoming a permanent ice sculpture. Every step of this process has been made possible thanks to the incredible Home Assistant community, who generously share their knowledge, tips, and life-saving YAML snippets. Sláinte! :ireland::maple_leaf:

2 Likes

That is bloody mad. I find it hard to finish a nice hot shower with turning the hot off.

This? Just WTF!!

1 Like