English · Español
A skeuomorphic 24-hour mechanical pool timer custom card for Home Assistant. Inspired by real analog pool timers, it lets you visually schedule your pool pump with interactive half-hour segments — plus presets (Summer / Winter) and timed treatment actions (flocculant / shock).
- 🪄 One-click setup — auto-creates the required helpers (admin) if missing, and fixes a too-small
max - 🎛️ 48 interactive segments (30 min each) — tap or drag to schedule (on touch devices a vertical swipe scrolls the page, a horizontal drag paints segments)
- ⏰ Real-time clock needle — auto-updates to show current time
- 🔄 3 operation modes: Auto / Perm / OFF
- 🗂️ Presets — one tap to load a full schedule (e.g. Summer / Winter), fully configurable
- 🌀 Flocculant action — circulate for N hours, then lock the pump OFF so the floc settles, until you vacuum the bottom and press resume
- 🧪 Treatment action (shock / product) — run for N hours, then automatically return to the previous mode
- 🔁 Retry with exponential backoff — verifies pump state after commands
- 🌐 Multi-language: English & Spanish (auto-detected from HA)
- 💾 Persistent — schedule, mode, presets and running actions stored in HA helpers; survive dashboard reloads
- 🤖 Automation-friendly — everything is stored in standard helpers
- 📱 Responsive — works on desktop and mobile
Important
Installing the card is half the setup. The pump is driven server-side by a blueprint, which is what makes the schedule run with every browser closed. Without it the card only acts when you tap it, and nothing enforces the schedule overnight.
There are three steps, not one:
- Install the card (below)
- Create the required helpers — the card can do this in one click
- Install the blueprint
Since v2.12.0 the card checks for the blueprint and tells you if it is missing, so you do not find out the night the pump does not run.
- Open HACS → Frontend → Custom repositories
- Add this repo URL, category: Lovelace
- Search for "Pool Timer Card" and install
- Refresh your browser (Ctrl+F5)
- Copy
pool-timer-card.jsto your/config/www/folder - In HA, go to Settings → Dashboards → Resources
- Add resource:
/local/pool-timer-card.js(JavaScript Module) - Refresh your browser
🪄 Automatic setup (easiest): when you add the card and any required helper is missing — or the schedule helper's
maxis below 48 — the card shows a "Create helpers" / "Fix it" button. If you are an admin, one click creates the helpers (and fixes themax) for you via Home Assistant's helper API. Non-admins get a note to ask an admin. You can still create them manually as described below.
Create these helpers in Settings → Devices & Services → Helpers.
| Field | Value |
|---|---|
| Name | pool_timer_schedule |
| Entity ID | input_text.pool_timer_schedule |
| Max length | 255 (must be at least 48) |
| Initial value | leave EMPTY |
⚠️ Important (length): the schedule is stored as a 48-character string. If the helper's maximum length is below 48, Home Assistant silently rejects the value and the schedule will never save. Set it to255to be safe. You can check the current limit in Developer Tools → States (themaxattribute).⛔ Important (persistence — do NOT set an Initial value): if a helper has an Initial value, Home Assistant resets it to that value on every restart instead of restoring the last value. Leaving it empty makes the helper keep its last value across restarts. Setting an initial here was the cause of the schedule resetting to all-off (and the mode reverting to Auto) after every HA restart. The card seeds the schedule automatically on first use.
| Field | Value |
|---|---|
| Name | pool_timer_mode |
| Entity ID | input_select.pool_timer_mode |
| Options | Auto, Perm, OFF |
| Initial value | leave EMPTY (do not set Auto; see warning above — otherwise the mode reverts to it on every restart) |
| Field | Value |
|---|---|
| Name | pool_timer_state |
| Entity ID | input_text.pool_timer_state |
| Max length | 255 |
| Initial value | (leave empty) |
This helper stores a small JSON blob describing the active preset and any
running timed action, e.g. {"preset":"Verano","action":"product","until":1718040000000,"ret":"Auto"}.
It is what lets a flocculant/treatment action resume correctly after a reload.
type: custom:pool-timer-card
entity: switch.pool_pump # Your pump switch entity (required)
name: Pool Timer # Card title (optional)
# Helpers (optional — these are the defaults)
schedule_entity: input_text.pool_timer_schedule
mode_entity: input_select.pool_timer_mode
state_entity: input_text.pool_timer_state
# Timed quick actions (optional — these are the defaults if omitted)
quick_actions:
- name: "Flocculant"
hours: 2
icon: "🌀"
after: "OFF"
- name: "Treatment"
hours: 3
icon: "🧪"
after: "Auto"
# Presets (optional — these are the defaults if omitted)
presets:
- name: Verano
schedule:
- { start: "08:00", end: "13:00" }
- { start: "16:00", end: "20:00" }
- name: Invierno
schedule:
- { start: "10:00", end: "13:00" }
# Corner quick-toggle buttons (optional — omit if you don't need them)
corner_actions:
- name: "Pool Lights"
icon: "💡"
position: "top-right" # tl, tr, bl, br (or top-left, top-right, …)
service: "light"
entity_id: "light.pool_lights"
action: "toggle"
- name: "Start Robot"
icon: "🤖"
position: "bottom-right"
service: "button" # press a one-shot button entity
entity_id: "button.pool_robot_start"
action: "press"| Option | Type | Required | Default | Description |
|---|---|---|---|---|
entity |
string | ✅ | — | Switch entity for the pool pump |
name |
string | ❌ | Pool Timer |
Card title |
schedule_entity |
string | ❌ | input_text.pool_timer_schedule |
Helper storing the 48-segment schedule |
mode_entity |
string | ❌ | input_select.pool_timer_mode |
Helper storing the operation mode |
state_entity |
string | ❌ | input_text.pool_timer_state |
Helper storing preset + running action |
preset_entity |
string | ❌ | — | input_select whose options are your preset names. Set it to store presets on the server (see Server-side presets) |
quick_actions |
list | ❌ | Flocculant, Treatment | Array of timed actions with name, hours, icon, after |
flocculant_hours |
number | ❌ | 2 |
(Legacy) Hours for flocculant action |
product_hours |
number | ❌ | 3 |
(Legacy) Hours for treatment action |
presets |
list | ❌ | Verano / Invierno | Named schedules; each has name + schedule ranges |
corner_actions |
list | ❌ | — | Corner quick-toggle buttons; each has name, icon, position, service, entity_id, action (see Corner Actions) |
schedule |
list | ❌ | — | One-time default schedule (used only when the schedule helper is empty) |
A preset can also be given as a raw 48-char string instead of ranges:
presets:
- name: Custom
segments: "111100000000111100000000000000000000000000000000"| Mode | Behavior |
|---|---|
| Auto | Pump follows the programmed schedule segments |
| Perm | Pump is always ON (override) |
| OFF | Pump is always OFF (override) |
Mode selector UI:
- With presets → dropdown menu (faster mode + preset switching)
- Without presets → 3 buttons (original UI)
Use the Presets dropdown (when you have presets configured) to:
- Select a preset to load its full 48-segment schedule and switch to Auto
- Choose "Custom" to edit segments manually on the dial
When you manually edit the dial, the selector switches to Custom so you know you're in edit mode — unless your edit happens to match a configured preset exactly, in which case the selector snaps to that preset automatically.
By default presets live in the card's YAML, which means only the card knows them: an automation that wants to apply "Verano" has to repeat its 48-character string, and the two drift apart the moment you edit one.
Set preset_entity and the presets move to Home Assistant instead, where
automations and the blueprint can read them:
type: custom:pool-timer-card
entity: switch.pool_pump
preset_entity: input_select.pool_timer_presetTwo kinds of entity back it:
| Entity | Holds |
|---|---|
The input_select you named |
Its options are the preset names, plus Custom |
One input_text per preset |
That preset's 48-character schedule |
The input_text for a preset is found by its friendly name — Pool Timer Preset <name> — not by its entity id. Home Assistant does not guarantee a
helper's entity id is derivable from its name, so the name is the link. Rename a
helper in Settings and the card treats that preset as missing.
Custom is not a preset. It is the absence of a match: the card selects it when
your schedule matches nothing, and the blueprint ignores it.
Press Create helpers on the card once. It creates the input_select and one
input_text per preset, seeded with the schedules your YAML holds today.
From then on the input_select's options are the preset list. Your presets: block
stops deciding which presets exist — it is only consulted to seed a helper you
recreate later, matching by name. That is why deleting a preset from the card makes
it disappear for good even though its YAML entry is still sitting there.
Nothing breaks if you never migrate: without preset_entity the card reads
presets: exactly as before.
With preset_entity set, the card's visual editor can add, edit and delete
presets.
Schedules are edited as time ranges, the same notation the YAML presets: uses:
10:00-14:00, 16:00-19:30
The card converts to and from the stored 48-character string for you. Two things follow from that conversion, both deliberate:
- Times snap to half hours, and the field re-renders from what was actually
stored. Type
10:15-14:20and it comes back as10:00-14:00— showing you the schedule the pump will follow rather than the one you typed. - Input that does not parse is rejected before anything is written, so a half-typed range never reaches the helper, and from there the pump.
A range crossing midnight round-trips intact: 23:00-01:00 stays one range.
Adding a preset creates the option and its helper, seeded with the schedule currently on the dial — so "+ Add Preset" means "save what I have".
Deleting takes two clicks and removes the option and its helper, so obsolete helpers do not pile up.
The name is read-only. Renaming would mean deleting and recreating the helper, so rename by deleting the preset and creating it again.
Tip
An option whose helper is missing, empty or malformed shows greyed out with a ⚠ and cannot be selected — selecting it would apply nothing while looking like it worked. Press Create helpers to create what is missing, or fill in its schedule.
Warning
Removing an option from Settings → Helpers instead of from the card's editor
leaves its input_text behind. Home Assistant has no idea the two are related — only
the card's ✕ button knows to delete both.
To keep dashboard scrolling smooth, the dial distinguishes gesture intent:
| Gesture | Action |
|---|---|
| Tap a segment | Toggle that half-hour on/off |
| Horizontal drag across the ring | Paint multiple segments |
| Vertical swipe | Scrolls the page (does not edit) |
Editing works in any mode, including OFF.
Fully configurable timed actions that override the schedule while active and
are stored in state_entity, so they survive a dashboard reload.
Define as many actions as you need:
quick_actions:
- name: "Flocculant"
hours: 2
icon: "🌀"
after: "OFF" # Lock pump OFF until manually resumed
- name: "Treatment"
hours: 3
icon: "🧪"
after: "Auto" # Return to Auto mode after
- name: "Filtrado"
hours: 1
icon: "🔄"
after: "Verano" # Return to the "Verano" presetParameters:
name— action label (e.g. "Flocculant", "Shock")hours— duration in hoursicon— emoji for the button (optional)after— what happens when the action finishes:"OFF"— lock the pump OFF (settling state) until user resumes"Auto"— return to Auto mode with the current schedule- preset name (e.g.
"Verano") — load that preset
Legacy format (still works):
flocculant_hours: 2
product_hours: 3- Tap an action button → pump runs for the configured hours.
- Timer counts down on the card.
- When time expires → applies the
afterbehavior. - You can cancel with the Cancel button anytime.
⏱️ Reliability note: the pump is driven server-side by the blueprint, so timed actions transition on time even with no browser open. Make sure you completed Required: install the blueprint — it is part of the setup, not optional.
Quick one-tap buttons positioned in any of the 4 corners of the dial for instant control of auxiliary entities: lights, jacuzzi, pool robot, filtration, etc. No timer — they execute immediately.
You can configure corner actions either from the visual editor (Edit card → Corner Actions section → + Add Corner Action, then set icon, name, corner, entity, service and action per row) or directly in YAML:
corner_actions:
- name: "Jacuzzi"
icon: "🛁"
position: "top-left" # tl, tr, bl, br (or full names: top-left, etc.)
service: "switch" # HA service domain
entity_id: "switch.jacuzzi" # Entity to control
action: "toggle" # toggle, turn_on, turn_off
- name: "Pool Lights"
icon: "💡"
position: "top-right"
service: "light"
entity_id: "light.pool_lights"
action: "toggle"
- name: "Pool Robot"
icon: "🤖"
position: "bottom-left"
service: "switch"
entity_id: "switch.pool_robot"
action: "turn_on"Parameters:
name— button label / tooltip (required)icon— emoji for the button (required)position— corner position (required):"tl"/"top-left","tr"/"top-right","bl"/"bottom-left","br"/"bottom-right"service— HA service domain:switch,light,button,script,scene,automation, etc.entity_id— the entity to control (required)action— what to do:toggle,turn_on,turn_off,press(forbuttonentities),trigger(for automations)
Pressing a button (e.g. a one-shot "start pool robot" button entity): set
service: button and action: press (the card sends button.press). button
and scene are normalized automatically, so even if the action is left at its
default they still fire correctly (button.press / scene.turn_on).
corner_actions:
- name: "Start Robot"
icon: "🤖"
position: "bottom-right"
service: "button"
entity_id: "button.pool_robot_start"
action: "press"Visual feedback:
- Default: icon only, subtle shadow
- On hover: icon scales up, glow effect
- Active (light is ON): icon color changes to green with glow
Limitations:
- Maximum 4 corner actions (one per corner)
- Each position can have at most one button
- Actions execute immediately (no timer, no state persistence)
When the card sends a command to turn the pump ON or OFF, it verifies the actual state after 2 seconds. If the state doesn't match:
- Retries with exponential backoff: 2s → 4s → 8s → 16s → 32s
- Shows an orange blinking LED during retries
- After 5 failed attempts, shows a fast red blinking LED for 10 seconds
- Automatically resets to normal monitoring afterwards
Everything is stored in standard helpers, so you can drive the card from HA.
# Switch to Perm mode when water temperature is high
automation:
- alias: "Pool pump on when hot"
trigger:
- platform: numeric_state
entity_id: sensor.pool_temperature
above: 30
action:
- service: input_select.select_option
target:
entity_id: input_select.pool_timer_mode
data:
option: "Perm"Important
To change the schedule, write the 48-character string to the schedule helper. Do not write a preset name into the state helper — that only changes the label shown in the dropdown, it does not change when the pump runs.
The schedule helper is the single source of truth: 48 characters, one per half hour,
starting at 00:00. 1 = pump on, 0 = pump off.
# Weekend schedule: 09:00-13:00 and 16:00-20:00
automation:
- alias: "Pool weekend schedule"
trigger:
- platform: time
at: "00:05:00"
condition:
- condition: time
weekday: [sat, sun]
action:
- service: input_text.set_value
target:
entity_id: input_text.pool_timer_schedule
data:
value: "000000000000000000111111110000001111111100000000"Every open card picks the change up immediately, and the blueprint enforces it with every browser closed. The preset dropdown updates on its own: the card matches the new schedule against your configured presets and shows that preset's name, or Custom when it matches none.
To make the bitstring easy to keep in sync with a preset, define presets with
segments: instead of schedule: — then the card config and your automation use the
exact same string:
presets:
- name: Summer
segments: "000000000000000011111111110000001111111100000000"Tip
Counting characters by hand is error-prone. To get the string for a schedule, set it
up on the dial and read the current value of input_text.pool_timer_schedule in
Developer tools → States, then paste it into your automation.
A common need: run a longer schedule at the weekend and a shorter one on working days — but only while you are in "summer", leaving your winter schedule alone.
With server-side presets this is the whole automation, with no schedule string in sight:
alias: Pool - summer schedule (short on weekdays, long at the weekend)
triggers:
- trigger: time
at: "00:00:00"
- trigger: homeassistant
event: start
conditions:
# Only while a summer preset is active — leaves Invierno or a hand-drawn
# schedule alone.
- condition: state
entity_id: input_select.pool_timer_preset
state: ["Verano", "Verano corto"]
actions:
- action: input_select.select_option
target:
entity_id: input_select.pool_timer_preset
data:
option: >-
{{ 'Verano' if now().isoweekday() in [6, 7] else 'Verano corto' }}
mode: singleEdit a preset on the dial and this automation applies the new version at the next switch, because it is the same data.
Without server-side presets (writing the schedule string directly)
alias: Pool - summer schedule (short on weekdays, long at the weekend)
description: >-
Alternates between the two summer schedules. Only acts when the current
schedule is already one of them, so a winter or hand-drawn schedule is left
untouched.
variables:
# 10:00-13:00 and 16:00-18:30
summer_short: "000000000000000000001111110000001111100000000000"
# 10:00-14:00 and 16:00-19:30
summer_long: "000000000000000000001111111100001111111000000000"
triggers:
- trigger: time
at: "00:00:00"
- trigger: homeassistant
event: start
conditions:
- condition: template
value_template: >-
{{ states('input_text.pool_timer_schedule') in [summer_short, summer_long] }}
actions:
- choose:
- conditions:
- condition: time
weekday: [sat, sun]
sequence:
- action: input_text.set_value
target:
entity_id: input_text.pool_timer_schedule
data:
value: "{{ summer_long }}"
- conditions:
- condition: time
weekday: [mon, tue, wed, thu, fri]
sequence:
- action: input_text.set_value
target:
entity_id: input_text.pool_timer_schedule
data:
value: "{{ summer_short }}"
mode: singleIt compares the schedule string, not the preset name. The state helper does carry
a preset field, but since 2.9.5 that name is derived by the card for display only —
with no dashboard open nothing keeps it up to date, so an automation that reads it will
eventually act on a stale value. The 48-character schedule is the source of truth.
It runs every day at 00:00 and on Home Assistant start, not only on the two switch days. On the other days it writes the value that is already there, which is a no-op. What this buys you is self-healing: if Home Assistant restarts on a Wednesday, or you change something by hand, you are back in sync at the next midnight instead of staying wrong until the following Monday.
The condition is what makes it safe. Without it, this automation would drag you back
into a summer schedule every night no matter what you had selected. With it, selecting
Invierno or drawing your own schedule simply opts you out until you pick a summer
schedule again.
No variable references another variable. Each choose branch reads a single
top-level variable. Chained variables (target: "{{ summer_long if ... }}") fail to
render on some Home Assistant versions and abort the run silently — the same trap the
blueprint warns about in its own comments.
[!TIP] Make the two strings exactly match presets you have configured. When they do, the card's dropdown shows
VeranoorVerano cortoafter the automation runs; when they don't, it shows Custom — which is correct, but tells you your automation and your presets have drifted apart.
[!NOTE] The schedule string is duplicated between your
presets:config and this automation, and nothing keeps the two in sync: editing a preset on the dial will not change what this automation applies. This is exactly what server-side presets remove — with them, the automation above collapses to a singleinput_select.select_option.
The card is the UI; the actual pump control runs server-side in Home Assistant via the included blueprint. This is what makes the schedule work 24/7 even with every browser/tab closed — without it, the card only reflects state and acts on your taps, but nothing enforces the schedule unattended.
⚠️ The blueprint is part of the setup, not optional. The card no longer drives the pump on a timer; it delegates that to the blueprint, which reads the same three helpers and reproduces the card's exact logic (Auto schedule, Perm/OFF, running actions, the flocculant "settling" lock, and the post-action transition).
…or in HA: Settings → Automations & scenes → Blueprints → Import blueprint, and paste:
https://github.com/serweck/pool-timer-card/blob/main/blueprints/pool_timer.yaml
Pick the four entities in the dropdowns — the pump switch and the three helpers
(schedule, mode, state). No YAML editing required.
A fifth input, Preset helper, is optional: set it to your preset_entity and
selecting a preset applies its schedule server-side, with no dashboard open. Leave
it empty if you have not moved presets to the server.
Important
Upgrading from a version before 2.10.0? Re-import the blueprint so the Preset helper input appears. Existing automations keep working untouched until you do — the new input defaults to empty.
That's it. The card stays a pure UI and the blueprint owns the pump; explicit taps on the card still act immediately for a snappy response, and the blueprint reconciles the state every minute (and right after a Home Assistant restart).
The card auto-detects your Home Assistant language and shows English or Spanish.
The schedule doesn't save / segments reset on reload
- Check the helper's max length (most common cause). Go to
Developer Tools → States, find
input_text.pool_timer_schedule, and look at themaxattribute. It must be ≥ 48. If it's lower, HA rejects the 48-character value. Fix it: Settings → Helpers → pool_timer_schedule → ⚙️ → Maximum length →255. - Confirm the helper accepts writes. In Developer Tools → Actions, run
input_text.set_valueon the entity with a 48-char value. If the state doesn't change, the helper is misconfigured. - Verify the card points at the right entities (
schedule_entity/mode_entity/state_entityin the card config). - Make sure you loaded the latest card — open the browser console (F12) and
confirm the
POOL-TIMER-CARD vX.Y.Zbanner; hard-refresh (Ctrl+Shift+R) if not.
Everything resets to defaults after restarting Home Assistant
This means one or more helpers have an Initial value set. HA resets a helper to
its initial value on every restart instead of restoring the last value. Fix each helper:
Settings → Devices & Services → Helpers → open pool_timer_schedule,
pool_timer_mode (and pool_timer_state) → ⚙️ → clear the Initial value field
(leave it empty) → Save. After that they keep their last value across restarts. (Card
v2.9.2+ also no longer overwrites the helper during the startup window.)
Presets or actions don't persist after a reload
Make sure input_text.pool_timer_state exists (helper #3) and its max is ≥ 200.
The mode buttons don't apply
Ensure input_select.pool_timer_mode exists and its options are exactly
Auto, Perm, OFF (case-sensitive).
MIT © serweck
