This script fades the volume of a target_player
media player, starting at it's current volume_level
, to a user-defined target_volume
over the user-defined duration
in seconds. It also applies one of three curve
algorithms to shape fade, defaulting to logarithmic
, which is often considered the most natural sounding fade.
I struggled to find any comprehensive solutions on the Home Assistant forums for fading media player volumes with common attenuation curves. I really don't like abrupt changes to audio volume, so I put this script together.
For those interested, the script is fully commented, and I've put together a quick explanation of the script's working below.
- target_player: media_player entity
- target_volume: float 0 - 1 --- volume to end at
- duration: float 0.1 - 60 --- duration of fade in seconds
- curve: selector [logarithmic, bezier, linear]
The script works first calculating the number of total_steps
required to fade based on the user-defined duration
multiplied by a hard-coded step_duration
of 100ms
(or 10 per second). For example, a duration of 5 seconds equates to 500 steps.
It determines the difference between the media player's current volume and the user-defined target_volume
. It applies the difference value as a factor to the shaped fade amount, adds it to the original volume, then pushes the new volume to the media player entity volume_level
for each step in a while
loop.
From what I could gather, Home Assistant calls its services on a 1 second clock. I don't know the details, however it's clear that sub-second delay calls aren't timed perfectly.
Issue: The lack of ms-accurate timing of Home Assistant's delay calls might cause slight discrepancies to fade durations. The larger the duration, the more noticeable this might become.
To make this script duration-accurate, instead of defining total_steps
, a steps_left
value could be used, defined by the script's start_time
, end_time
(which would be fixed), and the current_time
for each iteration of the loop. The repeat condition could then use a pre-defined end-time, with the fade steps increasing or decreasing depending on if the calls are lagging or ahead..... but I've already spent way too much time on this, so be my guest :)
Where x
is the normalised time value based on the duration
, starting at 0
and ending at 1
.
- (red) Linear:
f(x) = x
- (blue) Bezier:
f(x) = x / (1 + (1 - x ))
- (green) Logarithmic:
f(x) = x * x * (3 - 2x)
Add this to your Home Assistant script
config .yaml
, and use it anywhere that allows a service call (such as automations):
fade_volume:
alias: Fade the volume of a media player
mode: restart
fields:
target_player:
name: Target media player
description: Target media player of volume fade.
required: true
example: media_player.lounge_sonos
selector:
entity:
domain: media_player
target_volume:
name: Target volume
description: Volume the media play will be at the end of the fade duration.
required: true
default: 0.5
example: '0.5'
selector:
number:
max: 1
min: 0
step: 0.01
mode: slider
duration:
name: Fade duration
description: Length of time in seconds the fade should take.
required: true
default: 5
example: '5'
selector:
number:
mode: box
min: 0
max: 100000
unit_of_measurement: s
curve:
name: Fade curve algorithm
description: Shape of the fade curve to apply.
required: true
default: logarithmic
example: logarithmic
selector:
select:
options:
- logarithmic
- bezier
- linear
variables:
steps_per_second: 10
total_steps: '{{ (steps_per_second * duration) | int(0) }}'
start_volume: '{{ state_attr(target_player, ''volume_level'') | float(0) }}'
start_diff: '{{ (target_volume - start_volume) | float(0) }}'
sequence:
- repeat:
while:
- condition: template
value_template: '{{ repeat.index < total_steps }}'
- condition: template
value_template: '{{ ((state_attr(target_player, "volume_level") - target_volume)
| abs) > 0.001 }}'
sequence:
- service: media_player.volume_set
data_template:
entity_id: '{{ target_player }}'
volume_level: "{% set t = repeat.index / total_steps %} {% if curve == 'logarithmic'\
\ %}\n {{ (start_volume + (t / (1 + (1 - t))) * start_diff) | float(0)\
\ }}\n{% elif curve == 'bezier' %}\n {{ (start_volume + (t * t * (3 -\
\ 2 * t)) * start_diff) | float(0) }}\n{% else %}\n {{ (start_volume\
\ + t * start_diff) | float(0) }}\n{% endif %}\n"
- delay: '00:00:00.1'
- service: media_player.volume_set
data_template:
entity_id: '{{ target_player }}'
volume_level: '{{ target_volume }}'
icon: mdi:tune-vertical