Home AssistantESPHome

Hot water recirculation on demand

A morning window or bathroom motion runs a plug-in recirculation pump for a few minutes, or less when a return-pipe temperature sensor hits its target. Cooldown so motion cannot restart it constantly.

Difficulty · Beginner–IntermediateSetup · 45–90 minutesHA · current stable (blueprint)ESPHome · current stable
Get the pack · from $19Have us set it up

Some hardware links on this page are affiliate links. As an Amazon Associate I earn from qualifying purchases. We may earn a commission if you buy through them, at no extra cost to you. See the affiliate disclosure.

A mechanical circulator timer that runs fifteen minutes, eight times a day, keeps hot water “mostly” ready and spends the rest of its time stirring a loop that is already hot. A few minutes is usually enough for a domestic return loop, and it is too long if the return pipe is already at temperature. This pack turns a smart plug on from a morning schedule window or from bathroom motion, then turns it off when a return-pipe sensor reaches a target or when a short max runtime elapses, whichever comes first. A cooldown stops motion from starting another cycle while someone is still in the room.

Entity ids are blueprint inputs. Do not publish which room the motion sensor sits in beyond “a bathroom,” and do not publish a live plug name.

This is not relay auto-off timer. That pack starts a fixed duration whenever the switch turns on, with no temperature and no schedule window. This pack owns when the pump may start, and it can end the run early. Do not put both on the same plug.

What problem it solves

You want hot water at the far end of the loop during the hours people are up, plus an on-demand run when someone walks into a bathroom outside that rhythm, without leaving the pump on for a quarter-hour block or letting a motion sensor chatter it on and off.

How it works

  1. Two starts — a time pattern inside the morning window (pack default every 30 minutes between two clock times), or a motion binary going on. Both are optional inputs; leave motion empty for a schedule-only house.
  2. Cooldown — an input_datetime stores the last start. A new start is ignored until that timestamp is older than the cooldown (pack default 20 minutes). Motion that stays on does not queue another run. The schedule tick is subject to the same cooldown, so a window tick that lands during a run does nothing.
  3. Plug on — switch.turn_on the smart plug, then write now into the datetime. The pump’s own timer dial should be off or bypassed; Home Assistant owns the outlet.
  4. Stop early or on time — wait_for_trigger until the return-pipe temperature is at or above the target (pack default 38 °C / 100 °F — set it to the return temperature you actually measure when the far tap runs hot), with timeout equal to the max runtime (pack default 3 minutes). Then switch.turn_off.
  5. Sensor missing — if you did not add a probe, the wait is just the timeout. If the probe is configured and becomes unavailable mid-run, the timeout still fires. The pump cannot stay on because a sensor failed.
  6. Outside the window — motion still works if you enable it (that is the off-hours tap). The schedule does not. An optional “someone home” condition is in the pack; leave it empty to skip it. No names go in the blueprint.

A Home Assistant restart during a run can drop a wait_for_trigger. The pack also turns the plug off if it has been on for longer than the max runtime (a state trigger with for:), so a reboot cannot leave the circulator running until the next tick.

Hardware notes

  • Plug versus pump. This is for a plug-in circulator. Read the pump nameplate. The linked plug is 15 A at 120 V; a small domestic recirc is usually far under that, and a well pump or a hard-wired boiler circulator is a different job. Do not put a hard-wired pump on a smart plug by cutting into its mains.
  • Motion. Any binary_sensor with device class motion or occupancy. A PIR you already have is enough. The HC-SR501 is the “I do not have one” option: it can share the probe’s ESP32 or any other board. Set its onboard delay pot short and let Home Assistant own the cooldown — the module’s pot and this blueprint will fight if both try to be the timer.
  • Probe. Optional. A waterproof DS18B20 strapped to the return pipe (the line coming back to the heater, under the insulation, with the metal tip against copper) tells you the loop is hot. Supply water at the heater is the wrong pipe: it is hot even when the far bathroom is cold. The DIYables-style adapter includes the 4.7 kΩ pull-up; a bare probe needs that resistor between data and VCC.
  • Board. ESP32-C6 Super Mini if you are adding the probe. Leave GPIO8, GPIO9, GPIO12, GPIO13, and GPIO15 alone (LED, boot, USB, straps). The YAML uses GPIO4 for the probe and GPIO5 for an optional PIR.

Wi-Fi credentials stay in secrets.yaml. Never paste SSIDs, PSKs, or API keys into a published YAML.

Wiring

DS18B20 ESP32-C6 Super Mini
Red (VCC) 3V3
Black (GND) GND
Yellow or white (data) GPIO4
4.7 kΩ (if your board does not include it) Between data and 3V3
HC-SR501 (optional) ESP32-C6 Super Mini
VCC 5V
GND GND
OUT GPIO5

The PIR’s output is a 3.3 V high on most modules and is fine on a 3.3 V GPIO. Power it from 5V, not from 3V3.

Snippet

Blueprint shape. Inputs replace real entity ids:

trigger:
  - platform: time_pattern
    minutes: !input window_minutes
    id: schedule
  - platform: state
    entity_id: !input motion_sensor
    to: "on"
    id: motion
  - platform: state
    entity_id: !input pump_plug
    to: "on"
    for: !input max_runtime
    id: stuck_on

action:
  - choose:
      - conditions:
          - condition: trigger
            id: stuck_on
        sequence:
          - service: switch.turn_off
            target:
              entity_id: !input pump_plug
      - conditions:
          - condition: template
            value_template: "{{ cooldown_elapsed }}"
          - condition: or
            conditions:
              - condition: trigger
                id: motion
              - condition: and
                conditions:
                  - condition: trigger
                    id: schedule
                  - condition: time
                    after: !input window_start
                    before: !input window_end
        sequence:
          - service: switch.turn_on
            target:
              entity_id: !input pump_plug
          - service: input_datetime.set_datetime
            target:
              entity_id: !input last_start
            data:
              datetime: "{{ now() }}"
          - wait_for_trigger:
              - platform: numeric_state
                entity_id: !input return_temp
                above: !input target_temp
            timeout: !input max_runtime
            continue_on_timeout: true
          - service: switch.turn_off
            target:
              entity_id: !input pump_plug

Return-pipe node (substitutions, not a live house). One probe, so the address can be discovered in the logs instead of pasted:

substitutions:
  friendly_name: Return pipe
  data_pin: GPIO4
  motion_pin: GPIO5

one_wire:
  - platform: gpio
    pin: ${data_pin}
    id: pipe_bus

sensor:
  - platform: dallas_temp
    one_wire_id: pipe_bus
    name: ${friendly_name}
    unit_of_measurement: "°C"
    update_interval: 10s

binary_sensor:
  - platform: gpio
    pin:
      number: ${motion_pin}
      mode:
        input: true
        pullup: true
    name: Bathroom motion
    device_class: motion
    filters:
      - delayed_off: 3s

Skip the binary_sensor block if motion already exists somewhere else. Credentials stay in secrets.yaml.

Setup outline

  1. Confirm the pump is plug-in and the nameplate current is within the plug’s rating. Plug it in, adopt the plug in Home Assistant, and toggle it once while you are next to the pump so you know which outlet is live.
  2. Optional: flash the ESPHome YAML, strap the probe to the return pipe, and read a temperature that rises when you run the pump from the dashboard.
  3. Create the last-start input_datetime. Import the blueprint. Fill the plug, window, motion (or leave it blank), max runtime, cooldown, and the optional temperature target.
  4. Test schedule: set the window to include now and the pattern to every minute, run one cycle, confirm off at the timeout. Put the real 30-minute pattern back.
  5. Test motion: walk in, confirm one run, wave again inside the cooldown and confirm the plug stays off. If the probe is fitted, run a cycle with the target set just above the idle pipe temperature and confirm the plug drops before the timeout.

Troubleshooting

  • Pump never starts in the window. The time condition does not wrap, the cooldown datetime is still recent from a test, or the plug entity is a button rather than a switch. Window start must be earlier than window end for a same-day morning.
  • Pump never stops. A second automation or the plug’s own app timer turned it back on, or the stuck-on trigger is missing and a restart ate the wait. Keep a single owner, and keep the for: off trigger.
  • Stops in seconds. The target is below the idle return temperature, so the numeric trigger is already true. Measure the pipe cold, measure it after a real hot-tap run, and set the target between them.
  • Motion retriggers constantly. Cooldown is 0, or the PIR pot is retriggering and each new on edge bypasses your condition. The condition has to apply to the motion trigger, not only to the schedule.
  • Probe stays unknown. Data and VCC swapped, missing pull-up, or the pin is a strap pin. GPIO4 is the pack default. Power the probe from 3V3.
  • Both this pack and a duration timer fight. Remove relay auto-off timer from this plug.

Credits

Based on the community thread Scheduling a smart plug for a hot water circulation pump. This write-up is an original on-demand pack: a morning window or bathroom motion, a short max runtime, an earlier stop from a return-pipe sensor, and a cooldown.

Changelog

v1.0 · Oct 19, 2026: First publish.