Last active
April 27, 2026 19:14
-
-
Save mirisbowring/cb3e7c60a549d11866171287f97b4133 to your computer and use it in GitHub Desktop.
Schedules Pool Pump activity based on min activity per day, blocked times (like night) and has an option to steer the pump with solar overproduction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| blueprint: | |
| name: Pool Controller V6 (Auto-Cycle Calculation & PV) | |
| description: "Advanced pool pump controller using a grid sensor for PV surplus. Features anti-cycling delays, a daily minimum runtime guarantee, and an optional 'Force Cycles' toggle. The minimum cycle duration is automatically calculated based on pool volume and pump flow rate (1 full turnover). Requires an Input Select (Mode), Input Boolean (Force Cycle), and a History Stats sensor." | |
| domain: automation | |
| input: | |
| pool_pump_switch: | |
| name: Pool Pump Switch | |
| description: "The smart switch controlling your pool pump (e.g., Shelly)." | |
| selector: | |
| entity: | |
| domain: switch | |
| mode_select_entity: | |
| name: Mode Selector (Input Select) | |
| description: "Helper entity. Required options: 'Auto', 'Manual On', 'Manual Off', '24/7'." | |
| selector: | |
| entity: | |
| domain: input_select | |
| force_cycle_boolean: | |
| name: "Force Cycles Toggle (Input Boolean)" | |
| description: "If active, an ongoing pump cycle will not be interrupted by clouds until 1 full water turnover is completed." | |
| selector: | |
| entity: | |
| domain: input_boolean | |
| pool_volume: | |
| name: Pool Volume (m³) | |
| description: "Total volume of your pool in cubic meters." | |
| default: 6.0 | |
| selector: | |
| number: | |
| min: 1.0 | |
| max: 200.0 | |
| step: 0.5 | |
| pump_flow_rate: | |
| name: Pump Flow Rate (m³/h) | |
| description: "How much water your pump moves per hour." | |
| default: 6.0 | |
| selector: | |
| number: | |
| min: 0.5 | |
| max: 100.0 | |
| step: 0.5 | |
| runtime_sensor_entity: | |
| name: Runtime Sensor (History Stats) | |
| description: "Sensor tracking today's total pump runtime." | |
| selector: | |
| entity: | |
| domain: sensor | |
| grid_sensor_entity: | |
| name: Grid Power Sensor | |
| description: "Sensor reading your main meter. Negative values = Export/Surplus, Positive values = Import." | |
| selector: | |
| entity: | |
| domain: sensor | |
| start_time_input: | |
| name: Start Time (Window Open) | |
| description: "Earliest time the pump is allowed to run in Auto mode." | |
| default: "08:00:00" | |
| selector: | |
| time: | |
| end_time_input: | |
| name: End Time (Window Close) | |
| description: "Latest time the pump must stop in Auto mode." | |
| default: "20:00:00" | |
| selector: | |
| time: | |
| min_hours_input: | |
| name: Minimum Daily Runtime (Hours) | |
| description: "Target total runtime per day. Will force the pump on if time is running out." | |
| default: 10 | |
| selector: | |
| number: | |
| min: 1 | |
| max: 24 | |
| step: 1 | |
| pv_on_threshold_input: | |
| name: PV Turn-On Threshold (Watts) | |
| description: "Positive number. Start pump when exporting more than X watts." | |
| default: 600 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 10000 | |
| pv_on_delay: | |
| name: Turn-On Delay (Seconds) | |
| description: "Surplus must be stable for this long before starting." | |
| default: 60 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 3600 | |
| unit_of_measurement: s | |
| pv_off_threshold_input: | |
| name: PV Turn-Off Threshold (Watts) | |
| description: "Positive number. Stop pump when exporting drops below X watts." | |
| default: 200 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 10000 | |
| pv_off_delay: | |
| name: Turn-Off Delay (Seconds) | |
| description: "Time to wait before stopping. Bridges short clouds to prevent short-cycling." | |
| default: 180 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 3600 | |
| unit_of_measurement: s | |
| mode: restart | |
| trigger: | |
| - platform: state | |
| entity_id: !input mode_select_entity | |
| - platform: template | |
| value_template: "{{ (states(grid_sensor_entity) | float(0)) <= ((pv_on_threshold_input) * -1) }}" | |
| for: | |
| seconds: !input pv_on_delay | |
| - platform: template | |
| value_template: "{{ (states(grid_sensor_entity) | float(0)) > ((pv_off_threshold_input) * -1) }}" | |
| for: | |
| seconds: !input pv_off_delay | |
| - platform: time_pattern | |
| minutes: "/5" | |
| action: | |
| - variables: | |
| switch_entity: !input pool_pump_switch | |
| mode_entity: !input mode_select_entity | |
| force_entity: !input force_cycle_boolean | |
| pool_vol: !input pool_volume | |
| pump_flow: !input pump_flow_rate | |
| # Calculate duration for 1 full turnover in minutes | |
| cycle_min: "{{ ((pool_vol | float(1)) / (pump_flow | float(1))) * 60 }}" | |
| runtime_sensor: !input runtime_sensor_entity | |
| grid_sensor: !input grid_sensor_entity | |
| pv_on_threshold: !input pv_on_threshold_input | |
| pv_off_threshold: !input pv_off_threshold_input | |
| start_time: !input start_time_input | |
| end_time: !input end_time_input | |
| min_hours: !input min_hours_input | |
| current_mode: "{{ states(mode_entity) }}" | |
| force_active: "{{ is_state(force_entity, 'on') }}" | |
| # FIX: Calculate current continuous runtime in ONE step to avoid string conversion errors | |
| current_run_duration_min: "{{ ((now() - states[switch_entity].last_changed).total_seconds() / 60) if is_state(switch_entity, 'on') else 0 }}" | |
| # Logic: Cycle Protection | |
| is_cycle_finished: "{{ current_run_duration_min | float(0) >= cycle_min | float(0) }}" | |
| may_stop_by_pv: "{{ not force_active or is_cycle_finished }}" | |
| # Logic: Grid & PV calculations | |
| grid_val: "{{ states(grid_sensor) | float(0) }}" | |
| has_pv_surplus: "{{ grid_val <= (pv_on_threshold * -1) }}" | |
| is_below_stop_limit: "{{ grid_val > (pv_off_threshold * -1) }}" | |
| # Logic: Daily window and forced runs | |
| current_runtime_total: "{{ states(runtime_sensor) | float(0) }}" | |
| start_hhmm: "{{ start_time[:5] }}" | |
| end_hhmm: "{{ end_time[:5] }}" | |
| is_within_window: "{{ now().strftime('%H:%M') >= start_hhmm and now().strftime('%H:%M') < end_hhmm }}" | |
| hours_left_in_window: "{{ (today_at(end_hhmm) - now()).total_seconds() / 3600 if is_within_window else 0 }}" | |
| hours_left_today: "{{ (today_at('23:59') - now()).total_seconds() / 3600 }}" | |
| needs_forced_run_auto: "{{ is_within_window and (min_hours - current_runtime_total) >= hours_left_in_window }}" | |
| needs_forced_run_24_7: "{{ (min_hours - current_runtime_total) >= hours_left_today }}" | |
| - choose: | |
| # 1. MANUAL OFF | |
| - conditions: "{{ current_mode == 'Manual Off' }}" | |
| sequence: | |
| - service: switch.turn_off | |
| target: | |
| entity_id: "{{ switch_entity }}" | |
| # 2. MANUAL ON | |
| - conditions: "{{ current_mode == 'Manual On' }}" | |
| sequence: | |
| - service: switch.turn_on | |
| target: | |
| entity_id: "{{ switch_entity }}" | |
| # 3. AUTO MODE | |
| - conditions: "{{ current_mode == 'Auto' }}" | |
| sequence: | |
| - choose: | |
| # Fallback: Hard stop if outside window | |
| - conditions: "{{ not is_within_window }}" | |
| sequence: | |
| - service: switch.turn_off | |
| target: | |
| entity_id: "{{ switch_entity }}" | |
| # Turn ON: Forced run needed OR PV surplus is stable | |
| - conditions: "{{ needs_forced_run_auto or has_pv_surplus }}" | |
| sequence: | |
| - service: switch.turn_on | |
| target: | |
| entity_id: "{{ switch_entity }}" | |
| # Turn OFF: PV drops below limit AND no forced run needed AND cycle duration reached (if forced) | |
| - conditions: "{{ is_below_stop_limit and not needs_forced_run_auto and may_stop_by_pv }}" | |
| sequence: | |
| - service: switch.turn_off | |
| target: | |
| entity_id: "{{ switch_entity }}" | |
| # 4. 24/7 MODE | |
| - conditions: "{{ current_mode == '24/7' }}" | |
| sequence: | |
| - choose: | |
| # Turn ON: Forced run needed before midnight OR PV surplus is stable | |
| - conditions: "{{ needs_forced_run_24_7 or has_pv_surplus }}" | |
| sequence: | |
| - service: switch.turn_on | |
| target: | |
| entity_id: "{{ switch_entity }}" | |
| # Turn OFF: PV drops below limit AND no forced run needed AND cycle duration reached (if forced) | |
| - conditions: "{{ is_below_stop_limit and not needs_forced_run_24_7 and may_stop_by_pv }}" | |
| sequence: | |
| - service: switch.turn_off | |
| target: | |
| entity_id: "{{ switch_entity }}" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Smart PV Pool Pump Controller
A Home Assistant blueprint designed to control a pool pump based on solar (PV) surplus, while ensuring your pool water stays clean through a guaranteed daily minimum runtime and anti cycling protection.
Key Features
Auto,Manual On(Ignores PV and schedule),Manual Off(perfect for swimming), and24/7(ignores the night window).Prerequisites
Before importing this blueprint, you must create the following helper entities in the Home Assistant UI (Settings -> Devices & Services -> Helpers):
1. Mode Selector (Input Select)
Create an Dropdown helper with exactly these four options:
AutoManual OnManual Off24/72. Force Cycles Toggle (Input Boolean)
Create a Switch helper to enable or disable the "Force Cycle" protection.
3. Grid Power Sensor
You need a sensor reading your main house meter's active power in Watts.
4. Daily Runtime Sensor (History Stats)
Create a History Stats helper via the UI to track today's runtime.
on, the "Start time" to00:00:00of the current day, and the "End time" tonow().How "Force Cycles" Works
When the Force Cycles toggle is active, the blueprint calculates the time needed for one full water turnover (e.g., 60 minutes). If the pump starts due to solar surplus, but a cloud covers the sun 20 minutes later, the pump will not turn off. It will continue running until the 60-minute turnover is completed.
Note: The Night Quiet Time boundary overrides this and will always perform a hard stop to prevent nighttime noise.