I use white noise at night and wanted it to rise slowly before dawn and come down again in the morning. My first version used repeat with a one minute delay and volume_set each step. It worked until Home Assistant restarted in the middle of the night: the running ramp was cancelled and none of the later ramps happened, because the automation that “was waiting” no longer existed.
This blueprint avoids that by not waiting at all. Every minute (and right after Home Assistant starts) it:
- Works out which time block is active and what volume that block wants.
- Reads the speaker’s live volume.
- Moves it one step (default 1%) toward the target.
Because there is no long-running delay, a restart, a reload or a speaker dropping off Wi-Fi does not break anything. On the next minute it continues from whatever volume the speaker has.
Features
- Any number of time blocks, each with a start time and a target volume,
stop, ornone. - Optional media title filter (default
noise), so it only touches the speaker when the right thing plays. - Hand-change detection: if someone changes the volume, it pauses until the next block starts.
- Works with any
media_playerthat supportsvolume_set(tested on Google Nest speakers via Cast).
Setup
- Create two helpers per speaker: a Number helper (min 0, max 1, step 0.01), and a Text helper.
- Import the blueprint, choose the speaker and the two helpers, edit the blocks.
Example blocks (the default):
- {start: "19:00", volume: 70}
- {start: "04:00", volume: 85}
- {start: "05:30", volume: 70}
- {start: "06:30", volume: 60}
- {start: "07:30", volume: stop}
- {start: "08:00", volume: none}
A block lasts until the next one starts, and the last block of the day carries over past midnight.
Notes
- Starting the sound is up to you (a button, a voice routine, another automation). This blueprint only manages volume and the stop time.
- Google speakers usually do not chime on volume changes while media is playing, which is why it only acts while playing.
Source: Home Assistant blueprint: restart-proof volume schedule (ramp a speaker toward a target) · GitHub
Blueprint YAML
blueprint:
name: Restart-proof volume schedule (ramp a speaker toward a target)
description: >-
Keeps a media player at a scheduled volume and moves toward each new target
gradually (for example 1% per minute). Built for white noise or sleep sounds
that should get louder at night and quieter in the morning.
It is stateless: every minute (and right after Home Assistant starts) it reads
the speaker's live volume and takes one step toward the target for the current
time. Nothing waits in a long delay, so a restart or a speaker reconnecting never
breaks a ramp, it simply continues from the current volume.
If someone changes the volume by hand, the automation notices and leaves the
speaker alone until the next time block starts.
Needs two helpers you create once per speaker: a Number (0 to 1, step 0.01)
that stores the last volume this automation set, and a Text helper that stores
which block is paused.
domain: automation
input:
speaker:
name: Speaker
selector:
entity:
domain: media_player
blocks:
name: Time blocks
description: >-
List of blocks, each with a start time (HH:MM) and a target volume in percent.
Each block lasts until the next one starts. Use "stop" as the volume to stop
playback during that block, or "none" to leave the speaker alone.
default:
- {start: "19:00", volume: 70}
- {start: "04:00", volume: 85}
- {start: "05:30", volume: 70}
- {start: "06:30", volume: 60}
- {start: "07:30", volume: stop}
- {start: "08:00", volume: none}
selector:
object: {}
title_filter:
name: Only when the media title contains
description: Leave empty to control anything that plays.
default: noise
selector:
text: {}
step:
name: Step per minute (percent)
default: 1
selector:
number: {min: 1, max: 10, unit_of_measurement: "%"}
manual_tolerance:
name: Manual change tolerance (percent)
description: A difference larger than this from the last value set counts as a hand change.
default: 3
selector:
number: {min: 1, max: 20, unit_of_measurement: "%"}
last_set_helper:
name: Helper, last volume set (Number 0 to 1, step 0.01)
selector:
entity:
domain: input_number
hold_helper:
name: Helper, paused block (Text)
selector:
entity:
domain: input_text
mode: single
max_exceeded: silent
triggers:
- trigger: time_pattern
minutes: "/1"
- trigger: homeassistant
event: start
variables:
speaker: !input speaker
blocks: !input blocks
title_filter: !input title_filter
step: !input step
tolerance: !input manual_tolerance
last_e: !input last_set_helper
hold_e: !input hold_helper
hm: "{{ now().strftime('%H:%M') }}"
# The active block is the one with the latest start time not after now; before the
# earliest start, the block that started last (yesterday) is still active.
sorted_blocks: "{{ blocks | sort(attribute='start') }}"
current: >-
{% set past = sorted_blocks | selectattr('start', 'le', hm) | list %}
{{ (past | last) if past else (sorted_blocks | last) }}
block: "{{ current.start }}"
target: "{{ current.volume | string | lower }}"
vol: "{{ state_attr(speaker, 'volume_level') | float(-1) }}"
last: "{{ states(last_e) | float(-1) }}"
fresh: >-
{{ states[speaker] is not none
and (now() - states[speaker].last_changed).total_seconds() < 120 }}
conditions:
- condition: template
value_template: >-
{{ is_state(speaker, 'playing') and vol >= 0
and title_filter | lower in (state_attr(speaker, 'media_title') or '') | lower
and target != 'none' }}
actions:
- if: "{{ target == 'stop' }}"
then:
- action: media_player.media_stop
target: {entity_id: "{{ speaker }}"}
- stop: Scheduled stop
# A new block lifts any pause from a hand change.
- if: "{{ states(hold_e) not in ['', block] }}"
then:
- action: input_text.set_value
target: {entity_id: "{{ hold_e }}"}
data: {value: ""}
# Playback just started or the speaker just reconnected: adopt its current volume.
- if: "{{ fresh }}"
then:
- action: input_number.set_value
target: {entity_id: "{{ last_e }}"}
data: {value: "{{ vol | round(2) }}"}
- stop: Adopted the current volume
- condition: template
value_template: "{{ states(hold_e) != block }}"
- if: "{{ (vol - last) | abs > tolerance / 100 }}"
then:
- action: input_text.set_value
target: {entity_id: "{{ hold_e }}"}
data: {value: "{{ block }}"}
- stop: Volume changed by hand, paused until the next block
- variables:
t: "{{ (target | float(0)) / 100 }}"
s: "{{ step / 100 }}"
new: >-
{{ ([vol + s, t] | min) if vol < t - 0.005
else (([vol - s, t] | max) if vol > t + 0.005 else vol) }}
- condition: template
value_template: "{{ (new - vol) | abs > 0.004 }}"
- action: media_player.volume_set
target: {entity_id: "{{ speaker }}"}
data: {volume_level: "{{ new | round(2) }}"}
- action: input_number.set_value
target: {entity_id: "{{ last_e }}"}
data: {value: "{{ new | round(2) }}"}
Feedback welcome, especially from people using other speakers.