Home Assistant

Restart-proof volume ramp

Once a minute, and again when Home Assistant starts, step a playing speaker one notch toward the volume the current time block wants. Nothing sits in a long delay, so a restart continues from the live volume. A hand change pauses the ramp until the next block.

Difficulty · BeginnerSetup · 20–40 minutesHA · current stable (blueprint)
Get the pack · from $19Have us set it up

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

  1. Tick — time_pattern every minute, plus homeassistant start. There is no delay and no repeat held open overnight. mode: single is enough; a tick that is still running is skipped.
  2. 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.
  3. Targets — a number is a volume percent. stop stops the player for that block. none leaves the speaker alone (the ramp does not fight you during the day).
  4. 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_level is 0–1; the blocks are written in percent so the list stays readable.
  5. Playing only — the tick does nothing unless the player is playing. An optional title filter (pack default a substring such as noise) keeps the ramp off music that happens to be playing on the same speaker. An empty filter means any playback.
  6. 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.
  7. 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_player with volume_set and a volume_level attribute. 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 every volume_set will 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 one input_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

  1. Create the number helper (0–1, step 0.01) and the text helper. Leave the text helper empty.
  2. Confirm the player exposes volume_level while it is playing (Developer Tools → the entity).
  3. 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.
  4. 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.
  5. 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_set with 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_level live.
  • 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 leave media_title blank 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 not playing. none is 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.

Changelog

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