A fade built from repeat and delay dies when Home Assistant restarts. The automation that was waiting is gone, and every later step that night is gone with it. This workflow does not wait. Every minute, and again right after Home Assistant starts, it looks up the active time block, reads the speaker’s current volume, and moves one step toward that block’s target. A restart, a reload, or a speaker that dropped off Wi-Fi just means the next minute continues from whatever volume is actually playing.
Starting the audio is yours (a button, a script, a voice routine). This pack only owns volume, plus a block that can stop playback.
Entity ids are blueprint inputs. Do not publish a speaker name from a real room or a media title that identifies a library.
Clock on/off for a plug is daily device scheduler. A countdown that turns a switch off is relay auto-off timer. Neither of those ramps a volume.
What problem it solves
You want sleep audio or white noise to creep louder before dawn and back down in the morning, and you want that slope to survive a restart instead of freezing at whatever volume was set when Home Assistant stopped.
How it works
- Tick —
time_patternevery minute, plushomeassistantstart. There is nodelayand norepeatheld open overnight.mode: singleis enough; a tick that is still running is skipped. - Active block — you list blocks as a start time plus a target. The active block is the latest start time that is not still in the future. Before the day’s first start, yesterday’s last block is still active, so a schedule can cross midnight. Each block lasts until the next start.
- Targets — a number is a volume percent.
stopstops the player for that block.noneleaves the speaker alone (the ramp does not fight you during the day). - One step — if the live volume is below the target, add one step (pack default 1%). If it is above, subtract one step. If it is already on target, do nothing. Then store the value you set in a number helper (0 to 1, step 0.01). Home Assistant’s
volume_levelis 0–1; the blocks are written in percent so the list stays readable. - Playing only — the tick does nothing unless the player is
playing. An optional title filter (pack default a substring such asnoise) keeps the ramp off music that happens to be playing on the same speaker. An empty filter means any playback. - Hand change — if the live volume differs from the last value this automation stored by more than a tolerance (pack default a few percent), someone moved the knob. Write the current block id into a text helper and stop stepping until a new block starts, which clears the helper. A fresh playback (the player just changed, or it just reconnected) adopts the current volume into the helper instead of treating it as a hand change.
- Restart — the next minute, or the start event, reads the speaker again. There is no in-memory step counter to lose.
Hardware notes
- Player. Any
media_playerwithvolume_setand avolume_levelattribute. Cast speakers are the usual fit; they tend not to chime on each small step while audio is already playing. A player that beeps on everyvolume_setwill be annoying at one step per minute. Test one step by hand first. - Helpers. One
input_number(minimum 0, maximum 1, step 0.01) and oneinput_text, per speaker. Two speakers means two pairs and two automations. - No extra plug. Power for an amplifier is speaker plug follows playback. This pack assumes the player can already make sound.
Snippet
The pack ships a blueprint. Inputs replace real entity ids:
mode: single
max_exceeded: silent
trigger:
- platform: time_pattern
minutes: "/1"
- platform: homeassistant
event: start
action:
- variables:
target_pct: "{{ active_block_volume }}"
vol: "{{ state_attr(speaker, 'volume_level') | float(-1) }}"
last: "{{ states(last_set_helper) | float(-1) }}"
- condition: state
entity_id: !input speaker
state: "playing"
- choose:
- conditions:
- condition: template
value_template: "{{ target_pct == 'stop' }}"
sequence:
- service: media_player.media_stop
target:
entity_id: !input speaker
default:
- service: media_player.volume_set
target:
entity_id: !input speaker
data:
volume_level: "{{ next_step }}"
- service: input_number.set_value
target:
entity_id: !input last_set_helper
data:
value: "{{ next_step }}"
Example blocks, as a picture of the shape (edit the times and levels):
blocks:
- {start: "21:00", volume: 35}
- {start: "04:30", volume: 55}
- {start: "06:30", volume: 30}
- {start: "07:30", volume: stop}
- {start: "08:00", volume: none}
Setup outline
- Create the number helper (0–1, step 0.01) and the text helper. Leave the text helper empty.
- Confirm the player exposes
volume_levelwhile it is playing (Developer Tools → the entity). - Import the blueprint. Pick the player and both helpers. Enter the blocks. Set the title filter to a substring of the sleep-audio title, or clear it.
- Start playback inside a block whose target is far from the current volume. Once a minute the volume should move one step. Restart Home Assistant mid-ramp: within a minute it should step again from the volume you hear, not from zero and not from the original target in one jump.
- Move the volume by hand more than the tolerance. Stepping should stop until the next block’s start time.
Troubleshooting
- Volume jumps to the target in one go. Something else is calling
volume_setwith the final number, or the step is 100. This pack only adds or subtracts the step. - Ramp freezes after a restart. The automation is still the delay-loop version. The start trigger and the minute trigger have to both be present, and the action must read
volume_levellive. - Ramp fights a hand change. The tolerance is tighter than the player’s own rounding, so every tick looks like a hand change, or the opposite: the tolerance is so wide that a real twist is ignored. A few percent is the starting point. The text helper should show the block id while a hand pause is active.
- It ramps the wrong audio. The title filter is empty or does not match
media_title. Put a substring of the sleep track in the filter. Players that leavemedia_titleblank will never match a non-empty filter; clear the filter or pick a player that sets the title. - Nothing happens during the day. The active block is
none, or the player is notplaying.noneis intentional. Start the audio with your own script. - Midnight does the wrong block. The list is sorted by start time. The block with the latest start that is still earlier than now wins; if none of today’s starts have happened, the last block in the list is still in effect. Put the overnight target on the evening start, not on
00:00, if you want it held across midnight.
Credits
Based on the community blueprint Restart-proof volume schedule. This write-up is an original overview of the minute-tick, helper-backed ramp.