You know the one. Music is going in the kitchen, the washer finishes, Home Assistant says so, and then the music is gone. Or it comes back at announcement volume. Or the speaker that was grouped with the living room is now on its own. I wanted an announcement that leaves the house exactly the way it found it, so I wrote one.
This is a Home Assistant blueprint. You give it your speakers and a voice, then any automation can make an announcement by firing one event with a message. Before it says anything it writes down what every speaker was doing. After, it puts it all back.
What it does
- Takes a snapshot of each speaker: playing or paused, volume, mute, source, what was playing and how far into it.
- Pauses the music first, so turning the volume up for the message does not blast your song.
- Plays an optional chime, then the message, and waits for it to actually finish.
- Puts each speaker back: volume, mute, the stream or track it was on (and the spot in it), the input it was on, and paused speakers stay paused.
- Speakers that can announce on their own, like Music Assistant and Sonos, get the announcement the native way. The speaker lowers the music, says the message and carries on by itself. Everything else gets the snapshot and restore. You can force one way or the other if auto guesses wrong.
What it fixes
I read through the popular announcement blueprints and scripts on the Home Assistant forum before writing this one. Good work in there, but the same problems keep coming up in the threads:
- Only works on one brand. The best restore scripts lean on the Sonos snapshot service, so they do nothing for anything else. This one works with any media player.
- No restore at all. The general TTS blueprints play the message and leave the volume wherever it ended up.
- Two announcements at once. When a second message comes in while the first is playing, the second one takes its snapshot in the middle of the first one, so it “restores” your speakers to announcement volume. Here a second message waits its turn and takes its snapshot after the first one has put everything back. Or you can tell it to drop the second one.
- Speakers that are offline. An unavailable speaker gets skipped, even if it drops off halfway through the message. No error, the rest still work.
- Speakers that were off. Left off, by default. Or, if you want, they turn on for the message and go back off.
- Groups. If a speaker leaves its group to play the message, it goes back in after. If you pick both the group leader and a member, the member is not sent the message twice.
- Quiet hours. A lower volume overnight, and it handles a window that crosses midnight, like 10 PM to 7 AM.
- A restart halfway through. If Home Assistant restarts mid-message, the speakers can be left loud. Give it an optional text helper and it writes the volumes down first, then puts them back when Home Assistant comes up.
Setup
- Click the import button above, or in Home Assistant go to Settings, Automations & Scenes, Blueprints, Import Blueprint, and paste this address:
https://automatter.ca/wp-content/uploads/blueprints/announce_restore.yaml - Optional but worth it: make a text helper for restart recovery. Settings, Devices & Services, Helpers, Create Helper, Text. Set the maximum length to 255.
- Create an automation from the blueprint. Pick your speakers and your text-to-speech engine. Set the announcement volume, quiet hours if you want them, and the recovery helper.
- Make an announcement from any other automation or script by firing the event. In YAML it looks like the example below. In the visual editor, add an action, pick Other actions, then Event.
event: automatter_announce
event_data:
message: "The washer is done."
# optional, just for this one message:
# players: [media_player.kitchen, media_player.living_room]
# volume: 60
Want two separate setups, say upstairs and downstairs with different speakers? Create the automation twice and give each its own event name.
Why an automation and not a script
A script blueprint is a little easier to call, but a script cannot notice that Home Assistant just restarted. An automation can, so the restart recovery lives right in the same blueprint. Firing an event is one line, and it lets you queue or drop overlapping messages per setup.
What it was tested on
Home Assistant 2026.10.1, on a test install I run on a server, not in a real house. The speakers there are simulated: a small test integration that acts like real speakers (it plays a clip and then goes idle, a grouped speaker drops out of its group when you send it something directly, and one of them supports native announcements the way Music Assistant and Sonos do). Each one keeps a log of every command it gets, so I could check exactly what happened.
I ran 18 cases and 93 checks, and all of them passed: offline speakers before and during a message, speakers that were off, groups, two messages back to back in queue and drop mode, quiet hours inside, outside and across midnight, forced native and forced snapshot, a chime, and a full restart of Home Assistant in the middle of an announcement. The test caught one real bug along the way, the two-messages-at-once problem above, and that is fixed in this version.
Limitations, plainly
- It was not tested on real Sonos, Google, AirPlay, Alexa or ESPHome speakers, and not on a real Music Assistant server. The Music Assistant path is built on Home Assistant’s standard announce feature, and it was only tested against a simulated speaker that advertises that feature.
- Some music can’t be restarted from Home Assistant. Spotify Connect and some apps can’t be resumed just from what the speaker reports, so after the message that speaker may stay quiet. Native announce speakers don’t have this problem, because they resume the music on their own.
- On native announce speakers the script can’t see when the message ends, so it waits an estimate based on the length of the message.
- Restart recovery puts back volume and power. It does not restart the music.
- The recovery helper holds about six speakers. More than that and the extra ones are not saved for a restart (everything else still works).
- Two separate setups that share a speaker do not wait for each other.
The blueprint
Here is the whole thing, so you can read it before you import it. The same file is at https://automatter.ca/wp-content/uploads/blueprints/announce_restore.yaml.
# Built by AutoMatter | automatter.ca
# Write-up, updates and more Home Assistant blueprints: https://automatter.ca
blueprint:
name: Announce, then restore what was playing
author: AutoMatter (automatter.ca)
source_url: https://automatter.ca/wp-content/uploads/blueprints/announce_restore.yaml
description: >
Says a message (and an optional chime) on your speakers, then puts every speaker back
the way it was: volume, mute, source, what was playing and where it was in the track,
speaker groups, and speakers that were off go back off.
**How to use it:** create an automation from this blueprint, then from any other
automation or script fire the event named below (default `automatter_announce`) with
`message:` in the event data. Optional event data: `players:` (a list of speakers, to
override the default list for this one announcement) and `volume:` (0 to 100).
**What it handles:**
- Speakers that are unavailable or offline are skipped, never an error.
- Speakers that were off stay off (or, if you choose, wake up for the message and go back off).
- Speakers that can announce natively (Music Assistant, Sonos and others that support
`announce`) use that path, so the speaker ducks and resumes the music itself. Everything
else gets a full snapshot and restore.
- Grouped speakers are put back in their group afterwards.
- A second announcement while one is playing waits its turn (or is dropped, your choice).
It never snapshots the first announcement's volume by mistake.
- Quiet hours with their own volume, including hours that cross midnight.
- A Home Assistant restart in the middle of an announcement: with the optional recovery
helper, volumes and off speakers are put back when Home Assistant starts again.
Tested on Home Assistant 2026.10.1 with simulated speakers. Full write-up: [Announce, then put the music back](https://automatter.ca/announce-then-put-the-music-back-a-home-assistant-blueprint/)
domain: automation
homeassistant:
min_version: "2024.10.0"
input:
speakers_section:
name: Speakers
icon: mdi:speaker-multiple
input:
players:
name: Speakers
description: The speakers that get the announcement unless the event names others.
selector:
entity:
filter:
- domain: media_player
multiple: true
event_type:
name: Event name
description: >
The event this automation listens for. Leave it as is unless you want two
separate announcement setups (for example upstairs and downstairs), each with its
own event name.
default: automatter_announce
selector:
text:
sound_section:
name: Voice and chime
icon: mdi:account-voice
input:
tts_engine:
name: Text-to-speech engine
description: Any text-to-speech entity (Piper, Google Translate, Home Assistant Cloud and so on).
selector:
entity:
filter:
- domain: tts
tts_language:
name: Language (optional)
description: Leave blank to use the engine's default language. Example en-CA.
default: ""
selector:
text:
chime:
name: Chime before the message (optional)
description: >
A sound to play first, as a URL or a media-source id, for example
media-source://media_source/local/chime.mp3. Leave blank for no chime.
default: ""
selector:
text:
chime_seconds:
name: Chime length
default: 2
selector:
number:
min: 0
max: 15
step: 0.5
unit_of_measurement: s
volume_section:
name: Volume and quiet hours
icon: mdi:volume-high
input:
announce_volume:
name: Announcement volume
default: 45
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
quiet_hours:
name: Use quiet hours
default: false
selector:
boolean:
quiet_start:
name: Quiet hours start
default: "22:00:00"
selector:
time:
quiet_end:
name: Quiet hours end
description: Can be after midnight, for example 22:00 to 07:00.
default: "07:00:00"
selector:
time:
quiet_volume:
name: Quiet hours volume
default: 20
selector:
number:
min: 0
max: 100
unit_of_measurement: "%"
behaviour_section:
name: Behaviour
icon: mdi:cog
collapsed: true
input:
announce_method:
name: Announcement method
description: >
Auto uses the speaker's own announce feature when it has one (Music Assistant,
Sonos and others), and snapshot and restore for everything else. Pick a fixed
method only if auto guesses wrong for your speakers.
default: auto
selector:
select:
options:
- label: Auto (recommended)
value: auto
- label: Always snapshot and restore
value: snapshot
- label: Always the speaker's native announce
value: native
native_volume:
name: Volume on native-announce speakers
description: >
Music Assistant and Sonos set the announcement volume themselves. Leave it to the
speaker, or set the speaker volume like the others (the music may be louder for
the length of the message).
default: speaker
selector:
select:
options:
- label: Leave it to the speaker
value: speaker
- label: Set it like the other speakers
value: set
off_players:
name: Speakers that are off
default: skip
selector:
select:
options:
- label: Leave them off, skip them
value: skip
- label: Turn on for the message, then back off
value: wake
overlap:
name: When an announcement arrives while one is playing
default: queued
selector:
select:
options:
- label: Queue it, play it after
value: queued
- label: Drop it
value: single
max_seconds:
name: Longest wait for a message to finish
description: A safety limit. After this, the speakers are restored anyway.
default: 60
selector:
number:
min: 5
max: 600
unit_of_measurement: s
recovery_helper:
name: Recovery helper (optional, recommended)
description: >
An input_text helper (max length 255). The volumes are written here before the
announcement, so if Home Assistant restarts mid-message the speakers get their
volume back, and woken speakers go back off, when it starts again. Holds about
six speakers.
default: []
selector:
entity:
filter:
- domain: input_text
mode: !input overlap
max: 10
max_exceeded: silent
triggers:
- trigger: event
event_type: !input event_type
id: announce
- trigger: homeassistant
event: start
id: recover
variables:
# ---- inputs ----
default_players: !input players
tts_engine: !input tts_engine
tts_language: !input tts_language
chime: !input chime
chime_seconds: !input chime_seconds
announce_volume: !input announce_volume
quiet_hours: !input quiet_hours
quiet_start: !input quiet_start
quiet_end: !input quiet_end
quiet_volume: !input quiet_volume
method: !input announce_method
native_volume: !input native_volume
off_mode: !input off_players
max_seconds: !input max_seconds
recovery_helper: !input recovery_helper
# media_player feature flags
F_PAUSE: 1
F_SEEK: 2
F_VOLUME_SET: 4
F_MUTE: 8
F_TURN_ON: 128
F_TURN_OFF: 256
F_PLAY_MEDIA: 512
F_SELECT_SOURCE: 2048
F_PLAY: 16384
F_GROUPING: 524288
F_ANNOUNCE: 1048576
helper: >
{% set h = recovery_helper %}
{% if h is string %}{{ h }}{% elif h is iterable and h | count > 0 %}{{ h[0] }}{% else %}{% endif %}
data: "{{ trigger.event.data if trigger.id == 'announce' else {} }}"
message: "{{ (data.message | default('', true)) | string | trim }}"
targets: >
{% set p = data.players | default(default_players, true) %}
{% if p is string %}{% set p = p.split(',') %}{% endif %}
{{ p | map('trim') | reject('eq', '') | unique | list }}
actions:
- choose:
# ================= Restart recovery =================
- conditions:
- condition: template
value_template: "{{ trigger.id == 'recover' and helper != '' and states(helper) not in ['', 'unknown', 'unavailable'] }}"
sequence:
- delay: 5 # give integrations a moment to bring speakers back
- repeat:
for_each: "{{ states(helper).split(';') | reject('eq', '') | list }}"
sequence:
- variables:
parts: "{{ repeat.item.split('=') }}"
ent: "{{ parts[0] }}"
- if:
- condition: template
value_template: "{{ parts | count == 3 and states(ent) not in ['unavailable', 'unknown'] }}"
then:
- if:
- condition: template
value_template: "{{ parts[1] != '-' }}"
then:
- action: media_player.volume_set
continue_on_error: true
target:
entity_id: "{{ ent }}"
data:
volume_level: "{{ parts[1] | float }}"
- if:
- condition: template
value_template: "{{ parts[2] == '1' }}"
then:
- action: media_player.turn_off
continue_on_error: true
target:
entity_id: "{{ ent }}"
- action: input_text.set_value
continue_on_error: true
target:
entity_id: "{{ helper }}"
data:
value: ""
# ================= Announcement =================
- conditions:
- condition: template
value_template: "{{ trigger.id == 'announce' and (message != '' or chime != '') }}"
sequence:
# Everything below is read when this run STARTS, not when the event fired. With
# queued announcements that matters: the second one must snapshot the speakers after
# the first has put them back, never the first one's announcement volume.
- variables:
# quiet hours, including a window that crosses midnight (22:00 to 07:00)
now_hms: "{{ now().strftime('%H:%M:%S') }}"
in_quiet: >-
{{ quiet_hours and ((quiet_start <= now_hms < quiet_end) if quiet_start <= quiet_end
else (now_hms >= quiet_start or now_hms < quiet_end)) }}
level: >
{% set v = data.volume | default(quiet_volume if in_quiet else announce_volume, true) | float(45) %}
{{ ([0, v, 100] | sort)[1] / 100 }}
# ---- snapshot, taken before anything is touched ----
snap: >
{% set ns = namespace(out=[]) %}
{% set ma = integration_entities('music_assistant') %}
{% for p in targets if p.startswith('media_player.') %}
{% set s = states(p) %}
{% if s not in ['unavailable', 'unknown', 'none', '']
and not (s == 'off' and off_mode == 'skip') %}
{% set f = state_attr(p, 'supported_features') | int(0) %}
{% set can_native = (f | bitwise_and(F_ANNOUNCE) > 0) or p in ma %}
{% set native = method == 'native' or (method == 'auto' and can_native) %}
{% set pos = state_attr(p, 'media_position') %}
{% set upd = state_attr(p, 'media_position_updated_at') %}
{% if pos is number and s == 'playing' and upd %}
{% set pos = pos + (as_timestamp(now()) - as_timestamp(upd, as_timestamp(now()))) %}
{% endif %}
{% set g = state_attr(p, 'group_members') or [] %}
{% set ns.out = ns.out + [{
'e': p, 's': s, 'native': native,
'vol': state_attr(p, 'volume_level'),
'muted': state_attr(p, 'is_volume_muted') == true,
'src': state_attr(p, 'source'),
'cid': state_attr(p, 'media_content_id'),
'ctype': state_attr(p, 'media_content_type') or 'music',
'pos': pos if pos is number else none,
'group': g | list if g | count > 1 else [],
'wake': s == 'off',
'f': f }] %}
{% endif %}
{% endfor %}
{{ ns.out }}
snap_ids: "{{ snap | map(attribute='e') | list }}"
native_ids: "{{ snap | selectattr('native') | map(attribute='e') | list }}"
snapshot_ids: "{{ snap | rejectattr('native') | map(attribute='e') | list }}"
# A grouped speaker whose group leader is also getting the message hears it through the
# leader. Sending it the message too would play it twice and pull it out of the group.
play_ids: >
{% set ns = namespace(out=[]) %}
{% for x in snap if not x.native %}
{% if not (x.group | count > 1 and x.group[0] != x.e and x.group[0] in snapshot_ids) %}
{% set ns.out = ns.out + [x.e] %}
{% endif %}
{% endfor %}
{{ ns.out }}
tts_id: >
{% if message %}media-source://tts/{{ tts_engine }}?message={{ message | urlencode }}{{ '&language=' ~ tts_language if tts_language else '' }}{% endif %}
clip_seconds: >
{% set t = (chime_seconds | float(0) if chime else 0) + ((message | length) * 0.075 + 1.5 if message else 0) %}
{{ [t, max_seconds | float(60)] | min }}
- condition: template
value_template: "{{ snap | count > 0 }}"
# 1. Wake speakers that were off (only when asked to)
- repeat:
for_each: "{{ snap | selectattr('wake') | map(attribute='e') | list }}"
sequence:
- action: media_player.turn_on
continue_on_error: true
target:
entity_id: "{{ repeat.item }}"
- if:
- condition: template
value_template: "{{ snap | selectattr('wake') | list | count > 0 }}"
then:
- wait_template: >
{{ snap | selectattr('wake') | map(attribute='e') | map('states') | select('eq', 'off') | list | count == 0 }}
timeout: 5
# 2. Speakers that were off only report a volume once they are on
- variables:
wake_vols: >
{% set ns = namespace(d={}) %}
{% for x in snap if x.wake %}
{% set ns.d = dict(ns.d.items() | list + [(x.e, state_attr(x.e, 'volume_level'))]) %}
{% endfor %}
{{ ns.d }}
snap: >
{% set ns = namespace(out=[]) %}
{% for x in snap %}
{% set ns.out = ns.out + [dict(x, vol=(x.vol if x.vol is number else wake_vols.get(x.e)))] %}
{% endfor %}
{{ ns.out }}
# 3. Save volumes for restart recovery: entity=volume=wasoff;...
- if:
- condition: template
value_template: "{{ helper != '' }}"
then:
- action: input_text.set_value
continue_on_error: true
target:
entity_id: "{{ helper }}"
data:
value: >
{% set ns = namespace(s='') %}
{% for x in snap %}
{% set item = x.e ~ '=' ~ (x.vol if x.vol is number else '-') ~ '=' ~ (1 if x.wake else 0) ~ ';' %}
{% if (ns.s ~ item) | length <= 255 %}{% set ns.s = ns.s ~ item %}{% endif %}
{% endfor %}
{{ ns.s }}
# 4. Pause what is playing on snapshot speakers, so the volume change does not blast the music
- repeat:
for_each: >
{% set ns = namespace(out=[]) %}
{% for x in snap if not x.native and x.s == 'playing' and (x.f | bitwise_and(F_PAUSE)) > 0 %}
{% set ns.out = ns.out + [x.e] %}
{% endfor %}
{{ ns.out }}
sequence:
- action: media_player.media_pause
continue_on_error: true
target:
entity_id: "{{ repeat.item }}"
# 5. Unmute and set the announcement volume
- repeat:
for_each: "{{ snap }}"
sequence:
- variables:
x: "{{ repeat.item }}"
- if:
- condition: template
value_template: "{{ x.muted and (x.f | bitwise_and(F_MUTE)) > 0 }}"
then:
- action: media_player.volume_mute
continue_on_error: true
target:
entity_id: "{{ x.e }}"
data:
is_volume_muted: false
- if:
- condition: template
value_template: >
{{ (x.f | bitwise_and(F_VOLUME_SET)) > 0 and (not x.native or native_volume == 'set') }}
then:
- action: media_player.volume_set
continue_on_error: true
target:
entity_id: "{{ x.e }}"
data:
volume_level: "{{ level }}"
- variables:
t0: "{{ as_timestamp(now()) }}"
# 6. Native speakers: chime and message as announcements (the speaker resumes its music itself)
- repeat:
for_each: "{{ native_ids }}"
sequence:
- if:
- condition: template
value_template: "{{ chime != '' }}"
then:
- action: media_player.play_media
continue_on_error: true
target:
entity_id: "{{ repeat.item }}"
data:
media_content_id: "{{ chime }}"
media_content_type: music
announce: true
extra:
volume: "{{ (level * 100) | int }}"
- if:
- condition: template
value_template: "{{ chime != '' and message != '' and native_ids | count > 0 and snapshot_ids | count == 0 }}"
then:
- delay: "{{ chime_seconds }}"
- repeat:
for_each: "{{ native_ids if message != '' else [] }}"
sequence:
- action: media_player.play_media
continue_on_error: true
target:
entity_id: "{{ repeat.item }}"
data:
media_content_id: "{{ tts_id }}"
media_content_type: music
announce: true
extra:
volume: "{{ (level * 100) | int }}"
# 7. Snapshot speakers: play the chime then the message, waiting for each to finish
- repeat:
for_each: "{{ ([chime] if chime else []) + ([tts_id] if message else []) if play_ids | count > 0 else [] }}"
sequence:
- variables:
clip: "{{ repeat.item }}"
- repeat:
for_each: "{{ play_ids }}"
sequence:
- action: media_player.play_media
continue_on_error: true
target:
entity_id: "{{ repeat.item }}"
data:
media_content_id: "{{ clip }}"
media_content_type: music
# wait for it to start (max 5 s), then for it to stop
- wait_template: >
{{ play_ids | select('is_state', 'playing') | list | count > 0 }}
timeout: 5
- wait_template: >
{{ play_ids | select('is_state', 'playing') | list | count == 0 }}
timeout: "{{ [max_seconds | float(60), 5] | max }}"
# 8. Make sure native announcements have had time to finish
- delay: "{{ [0, clip_seconds | float(0) - (as_timestamp(now()) - t0 | float(0))] | max | round(1) }}"
# 9. Restore each speaker
- repeat:
for_each: "{{ snap }}"
sequence:
- variables:
x: "{{ repeat.item }}"
- if:
- condition: template
value_template: "{{ states(x.e) not in ['unavailable', 'unknown'] }}"
then:
# 9a. back into its group (leader re-joins the others that are still around)
- if:
- condition: template
value_template: >
{{ x.group | count > 1 and x.group[0] == x.e
and (state_attr(x.e, 'group_members') or []) | sort != x.group | sort }}
then:
- action: media_player.join
continue_on_error: true
target:
entity_id: "{{ x.e }}"
data:
group_members: >
{{ x.group[1:] | reject('is_state', 'unavailable') | list }}
# 9b. a member whose leader was not part of the announcement re-joins it
- if:
- condition: template
value_template: >
{{ x.group | count > 1 and x.group[0] != x.e and x.group[0] not in snap_ids
and x.e not in (state_attr(x.group[0], 'group_members') or []) }}
then:
- action: media_player.join
continue_on_error: true
target:
entity_id: "{{ x.group[0] }}"
data:
group_members: "{{ [x.e] }}"
# 9c. volume and mute
- if:
- condition: template
value_template: >
{{ x.vol is number and (x.f | bitwise_and(F_VOLUME_SET)) > 0
and (not x.native or native_volume == 'set') }}
then:
- action: media_player.volume_set
continue_on_error: true
target:
entity_id: "{{ x.e }}"
data:
volume_level: "{{ x.vol }}"
- if:
- condition: template
value_template: "{{ x.muted and (x.f | bitwise_and(F_MUTE)) > 0 }}"
then:
- action: media_player.volume_mute
continue_on_error: true
target:
entity_id: "{{ x.e }}"
data:
is_volume_muted: true
# 9d. what was playing (snapshot speakers only; a group member follows its leader)
- choose:
- conditions:
- condition: template
value_template: "{{ x.wake }}"
sequence:
- action: media_player.turn_off
continue_on_error: true
target:
entity_id: "{{ x.e }}"
- conditions:
- condition: template
value_template: >
{{ not x.native and x.s in ['playing', 'paused']
and not (x.group | count > 1 and x.group[0] != x.e) }}
sequence:
- choose:
# a stream or track we can reload
- conditions:
- condition: template
value_template: "{{ x.cid and (x.f | bitwise_and(F_PLAY_MEDIA)) > 0 }}"
sequence:
- action: media_player.play_media
continue_on_error: true
target:
entity_id: "{{ x.e }}"
data:
media_content_id: "{{ x.cid }}"
media_content_type: "{{ x.ctype }}"
- if:
- condition: template
value_template: "{{ x.pos is number and x.pos > 5 and (x.f | bitwise_and(F_SEEK)) > 0 }}"
then:
- wait_template: "{{ is_state(x.e, 'playing') }}"
timeout: 5
- action: media_player.media_seek
continue_on_error: true
target:
entity_id: "{{ x.e }}"
data:
seek_position: "{{ x.pos | round(0) }}"
# an input (line in, TV, radio preset) we can switch back to
- conditions:
- condition: template
value_template: "{{ x.src and (x.f | bitwise_and(F_SELECT_SOURCE)) > 0 }}"
sequence:
- action: media_player.select_source
continue_on_error: true
target:
entity_id: "{{ x.e }}"
data:
source: "{{ x.src }}"
# it was paused: leave it paused where it was
- if:
- condition: template
value_template: "{{ x.s == 'paused' and (x.f | bitwise_and(F_PAUSE)) > 0 }}"
then:
- wait_template: "{{ is_state(x.e, 'playing') }}"
timeout: 5
- action: media_player.media_pause
continue_on_error: true
target:
entity_id: "{{ x.e }}"
# 10. Done: clear the recovery note
- if:
- condition: template
value_template: "{{ helper != '' }}"
then:
- action: input_text.set_value
continue_on_error: true
target:
entity_id: "{{ helper }}"
data:
value: ""
Tested on real hardware
Beyond the test bench, this was run on a real HomePod mini playing through Music Assistant: the volume came up for the message, the music kept playing quietly underneath (proper ducking), the message played, and the music came back to exactly where it was with the same song still playing. That real test also caught a bug the simulated speakers never showed (Music Assistant reports some values in a format that broke the saved snapshot), and it is fixed in the current version.
One limit to know: if a speaker is getting music from another device over AirPlay (your phone or Mac playing straight to it), an announcement takes the speaker over and that device gets disconnected. Home Assistant cannot tell your phone to start again, so you would need to press play. Music started through Music Assistant comes back on its own.
