Workaround: Reliable TTS / Long TTS Playback to Apple HomePods Using Pre-Generation + Local Media
TL;DR
I ran into an issue where TTS announcements—particularly longer Google AI TTS messages—would intermittently or consistently fail when sent to Apple HomePods through the Apple TV integration.
The failures included TTS generation/playback timeouts and, when attempting to work around those by pre-generating the audio, errors such as:
Streaming the requested media is not supported
I eventually found a reliable workaround:
Generate TTS completely first
↓
Get the generated /api/tts_proxy/...mp3 URL
↓
Download/copy that completed MP3 into /media
↓
Play it using Home Assistant's local media source
↓
HomePod
In other words:
Don’t make the HomePod wait for the TTS engine to generate the audio, and don’t ask the HomePod to directly consume the temporary TTS proxy URL.
Instead, pre-generate the speech, copy the resulting MP3 into Home Assistant’s local media directory, and then give the HomePod a normal media-source:// local-media object.
This has been reliable in my testing, and playback begins quickly.
This is a workaround, not necessarily a permanent solution. There are existing reports involving HomePod/AirPlay TTS failures and approximately 10-second timeouts, and there appears to be ongoing work around the underlying Apple TV/HomePod audio behavior. A future Home Assistant release may make this workaround unnecessary.
Environment Where This Was Tested
My environment at the time of testing:
| Component | Configuration |
|---|---|
| Hardware | Home Assistant Green |
| Home Assistant Core | 2026.8.2 |
| Supervisor | 2026.07.5 |
| Home Assistant OS | 18.2 |
| Frontend | 20260729.7 |
| Speakers | Apple HomePods |
| HomePod integration | Apple TV integration |
| Network | HA Green and HomePods on the same VLAN |
| Firewall between HA/HomePods | None |
| HACS | Not used |
The network detail is important.
The HomePods and Home Assistant Green are on the same VLAN with no network restrictions between them. Therefore, in my environment this was not an inter-VLAN firewall, mDNS, multicast, or ACL problem.
The HomePods are integrated directly into Home Assistant using the standard Apple TV integration.
The Problem
I originally encountered this while building more sophisticated spoken automations.
Two examples that led me down this path were:
- Calendar/travel announcements — Home Assistant determines the travel time to an upcoming calendar event and announces when it is time to prepare to leave.
- Coffee-machine news brief — when the coffee machine starts, Home Assistant creates a personalized calendar/weather or news briefing and speaks it over a HomePod.
These aren’t necessarily short messages such as:
“The garage door is open.”
They can be relatively long, dynamically generated TTS messages.
Google AI TTS could generate excellent speech, but the complete process looked roughly like this:
Automation
↓
Generate TTS
↓
Home Assistant / pyatv
↓
HomePod
Longer generation could collide with the timeout behavior seen in the HA/HomePod playback path.
Interestingly, I also observed this behavior during testing:
Long TTS message
First attempt → fails
Wait ~10 seconds
Same message again → works
That was a major clue.
By the second attempt, the audio had already been generated/cached.
The First Workaround: Pre-Generate the TTS
Instead of asking tts.speak to generate and play everything synchronously, I used Home Assistant’s TTS URL API to generate the audio first.
The resulting response looked similar to:
status: 200
content:
url: http://HOME_ASSISTANT_IP:8123/api/tts_proxy/xxxxxxxxxxxxxxxx.mp3
path: /api/tts_proxy/xxxxxxxxxxxxxxxx.mp3
At this point, the expensive TTS-generation step was complete.
However, sending that URL directly to the HomePod with media_player.play_media resulted in:
Streaming the requested media is not supported
So pre-generation solved one problem but exposed another.
What Finally Worked
I manually copied one of those generated MP3 files into Home Assistant’s /media directory.
Then I played it using Home Assistant’s local media source:
action: media_player.play_media
target:
entity_id: media_player.your_homepod
data:
media_content_id: media-source://media_source/local/tts-test.mp3
media_content_type: audio/mpeg
It played immediately.
That established a reliable path:
TTS engine
↓
HA TTS proxy
↓
Completed MP3
↓
/media
↓
HA Media Source
↓
Apple TV integration
↓
HomePod
The key difference is that the HomePod is no longer involved while the TTS audio is being generated.
By the time the HomePod receives the playback request, a complete MP3 already exists in Home Assistant’s local media storage.
Before Continuing — Important Warning
Use extreme caution before proceeding.
The following procedure involves editing
configuration.yaml, creating shell scripts, changing file permissions, and executing commands within your Home Assistant environment.Make a backup first and use Developer Tools → YAML → Check configuration before restarting Home Assistant.
Commands and filesystem paths may differ depending on your Home Assistant installation type.
Proceed at your own risk.
These examples were tested on Home Assistant OS running on Home Assistant Green. Container, Core, or other installation types may require changes, particularly around
/mediastorage and filesystem access.
Step-by-Step Solution
Step 1 — Create a Directory for Generated Announcements
SSH into Home Assistant and create a dedicated directory:
mkdir -p /media/tts_announcements
Verify it:
ls -ld /media/tts_announcements
Why?
We want the generated speech to become a normal Home Assistant local-media object.
Keeping generated announcements in their own directory also makes troubleshooting and cleanup much easier.
Step 2 — Create a Script That Copies Generated TTS into /media
Create a directory for shell scripts if necessary:
mkdir -p /config/shell
Create the script:
nano /config/shell/copy_tts_to_media.sh
Add:
#!/bin/sh
set -eu
SOURCE_URL="$1"
FILENAME="$2"
TARGET_DIR="/media/tts_announcements"
TMP_FILE="${TARGET_DIR}/${FILENAME}.tmp"
FINAL_FILE="${TARGET_DIR}/${FILENAME}"
mkdir -p "$TARGET_DIR"
curl -fsSL \
--connect-timeout 10 \
--max-time 30 \
"$SOURCE_URL" \
-o "$TMP_FILE"
if [ ! -s "$TMP_FILE" ]; then
echo "Downloaded TTS file is empty" >&2
rm -f "$TMP_FILE"
exit 1
fi
mv "$TMP_FILE" "$FINAL_FILE"
echo "$FINAL_FILE"
Save the file and make it executable:
chmod 755 /config/shell/copy_tts_to_media.sh
Why Use a Temporary File?
This is intentional:
announcement.mp3.tmp
↓
download completes
↓
announcement.mp3
The final filename doesn’t exist until the download has successfully completed.
That prevents a media player from attempting to retrieve an MP3 while it is still being written.
The final mv effectively becomes the handoff:
Downloading → not ready
Final MP3 exists → ready for playback
Step 3 — Expose the Script as a shell_command
Add this to configuration.yaml:
shell_command:
copy_tts_to_media: >-
/bin/sh /config/shell/copy_tts_to_media.sh
"{{ source_url }}"
"{{ filename }}"
Important
If you already have:
shell_command:
do not add a second shell_command: section.
Instead, add the new command beneath the existing one:
shell_command:
existing_command: ...
copy_tts_to_media: >-
/bin/sh /config/shell/copy_tts_to_media.sh
"{{ source_url }}"
"{{ filename }}"
Why Use a Separate Script?
I initially attempted something similar to:
shell_command:
copy_tts: >-
mkdir -p /media/tts_announcements &&
curl -fsSL ...
That failed unexpectedly.
Home Assistant restricts how templated shell_command commands are executed. Compound shell behavior can therefore behave differently than expected.
Putting the more complicated shell logic inside:
/config/shell/copy_tts_to_media.sh
and having shell_command invoke that script proved much more reliable.
After adding the command:
Developer Tools → YAML → Check configuration
Then reload the Shell Command integration or restart Home Assistant as appropriate.
Step 4 — Pre-Generate the TTS
The goal is to make the TTS engine finish before the HomePod becomes involved.
In my case, I used a REST command that calls Home Assistant’s TTS URL API.
Conceptually:
rest_command:
pregenerate_tts:
url: "http://127.0.0.1:8123/api/tts_get_url"
method: POST
timeout: 30
headers:
authorization: !secret ha_rest_bearer_token
content-type: "application/json"
payload: >-
{
"engine_id": "YOUR_TTS_ENTITY",
"message": {{ message | tojson }},
"cache": true,
"language": "en-US"
}
content_type: "application/json"
Replace:
YOUR_TTS_ENTITY
with the appropriate TTS engine/entity for your environment.
My implementation uses Google AI TTS, but the important concept here isn’t Google specifically.
The architecture is:
Generate first
↓
Wait for completion
↓
Obtain finished audio
↓
Then start HomePod playback
I intentionally use:
timeout: 30
for the REST generation request because longer AI-generated speech can take more than 10 seconds.
Protect Your Token
Do not publish your bearer token directly in configuration.yaml.
Store it in secrets.yaml and reference it with:
authorization: !secret ha_rest_bearer_token
Step 5 — Read the Generated TTS URL
The response variable should contain something resembling:
status: 200
content:
url: http://HOME_ASSISTANT_IP:8123/api/tts_proxy/xxxxxxxx.mp3
path: /api/tts_proxy/xxxxxxxx.mp3
An important discovery during troubleshooting:
content may already be a parsed object.
I originally attempted:
{{ (tts_generation.content | from_json).url }}
That failed with:
Template error: from_json got invalid input
because Home Assistant had already parsed the response.
Instead:
- variables:
tts_ok: >
{{
tts_generation is defined
and
tts_generation.status | int(0) == 200
and
tts_generation.content is defined
and
tts_generation.content.url | default('') | length > 0
}}
tts_url: >
{% if tts_ok %}
{{ tts_generation.content.url }}
{% else %}
{{ '' }}
{% endif %}
Step 6 — Copy the Completed MP3 into Local Media
After TTS generation succeeds:
- action: shell_command.copy_tts_to_media
data:
source_url: "{{ tts_url }}"
filename: "announcement.mp3"
response_variable: copy_result
A successful result should resemble:
stdout: /media/tts_announcements/announcement.mp3
stderr: ""
returncode: 0
I recommend adding a small delay before playback:
- delay:
seconds: 1
This isn’t necessarily required on every system, but it provides a small buffer between the completed file operation and the media request.
Step 7 — Play the Local Media File
Now give the HomePod the Home Assistant Media Source URI instead of the TTS proxy URL:
- action: media_player.play_media
target:
entity_id: media_player.your_homepod
data:
media_content_id: >
media-source://media_source/local/tts_announcements/announcement.mp3
media_content_type: audio/mpeg
That media-source URI corresponds to:
/media/tts_announcements/announcement.mp3
In my testing, this was the important breakthrough.
The HomePod retrieved and played the local-media MP3 quickly.
Step 8 — Use Unique Filenames
For dynamic automations, I recommend not continually overwriting the same file.
For example:
- variables:
tts_filename: >
announcement_{{ now().strftime('%Y%m%d_%H%M%S') }}.mp3
Then:
- action: shell_command.copy_tts_to_media
data:
source_url: "{{ tts_url }}"
filename: "{{ tts_filename }}"
And:
- action: media_player.play_media
target:
entity_id: media_player.your_homepod
data:
media_content_id: >
media-source://media_source/local/tts_announcements/{{ tts_filename }}
media_content_type: audio/mpeg
This avoids potential cache collisions and makes troubleshooting easier because you can see exactly which generated file belongs to which automation execution.
Step 9 — Clean Up Old Generated Files
You probably don’t want hundreds of generated announcements accumulating indefinitely.
Create:
nano /config/shell/cleanup_tts_media.sh
Add:
#!/bin/sh
set -eu
TARGET_DIR="/media/tts_announcements"
mkdir -p "$TARGET_DIR"
find "$TARGET_DIR" -type f \
\( -name "*.mp3" -o -name "*.tmp" \) \
-delete
Make it executable:
chmod 755 /config/shell/cleanup_tts_media.sh
Then add another shell command:
shell_command:
cleanup_tts_media: >-
/bin/sh /config/shell/cleanup_tts_media.sh
Again, if you already have a shell_command: section, add this beneath it rather than creating another one.
You can call the cleanup command from a nightly automation or use whatever retention schedule makes sense for your environment.
Complete Automation Pattern
The important portion of an automation ends up looking roughly like this:
- variables:
announcement: >
This is a longer dynamically generated Home Assistant
announcement that I want to play on my HomePod.
tts_filename: >
announcement_{{ now().strftime('%Y%m%d_%H%M%S') }}.mp3
- action: rest_command.pregenerate_tts
data:
message: "{{ announcement }}"
response_variable: tts_generation
- variables:
tts_ok: >
{{
tts_generation is defined
and
tts_generation.status | int(0) == 200
and
tts_generation.content is defined
and
tts_generation.content.url | default('') | length > 0
}}
tts_url: >
{% if tts_ok %}
{{ tts_generation.content.url }}
{% else %}
{{ '' }}
{% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ tts_ok }}"
sequence:
- action: shell_command.copy_tts_to_media
data:
source_url: "{{ tts_url }}"
filename: "{{ tts_filename }}"
response_variable: copy_result
- delay:
seconds: 1
- action: media_player.play_media
target:
entity_id: media_player.your_homepod
data:
media_content_id: >
media-source://media_source/local/tts_announcements/{{ tts_filename }}
media_content_type: audio/mpeg
The full architecture is therefore:
┌──────────────────────────────┐
│ Automation builds TTS text │
└──────────────┬───────────────┘
↓
┌──────────────────────────────┐
│ TTS generation completes │
│ before HomePod is involved │
└──────────────┬───────────────┘
↓
┌──────────────────────────────┐
│ /api/tts_proxy/...mp3 │
└──────────────┬───────────────┘
↓
┌──────────────────────────────┐
│ shell_command + curl │
│ downloads completed MP3 │
└──────────────┬───────────────┘
↓
┌──────────────────────────────┐
│ /media/tts_announcements/ │
│ announcement_xxx.mp3 │
└──────────────┬───────────────┘
↓
┌──────────────────────────────┐
│ HA Media Source │
│ media-source://... │
└──────────────┬───────────────┘
↓
┌──────────────────────────────┐
│ Apple TV integration │
└──────────────┬───────────────┘
↓
🔊 HomePod
Manual Testing / Troubleshooting
I strongly recommend testing each layer independently before putting this into a large automation.
This made troubleshooting considerably easier for me.
Test 1 — Confirm /media Works
Put a known-good MP3 in:
/media/tts_announcements/test.mp3
Verify:
ls -lh /media/tts_announcements/test.mp3
You should also be able to locate local media through Home Assistant’s Media interface.
Test 2 — Play the MP3 Manually
Go to:
Developer Tools → Actions
Run:
action: media_player.play_media
target:
entity_id: media_player.your_homepod
data:
media_content_id: >
media-source://media_source/local/tts_announcements/test.mp3
media_content_type: audio/mpeg
If this fails, stop here.
At this point the problem isn’t TTS generation.
Verify:
- Apple TV integration
- HomePod connectivity
- Local Media configuration
- MP3 compatibility
- Home Assistant’s ability to expose
/media
This test is important because it isolates the HomePod/local-media portion from everything else.
Test 3 — Generate TTS Without Playing It
Run:
action: rest_command.pregenerate_tts
data:
message: >
This is a test of pre-generated text to speech.
response_variable: test_tts
You want a response resembling:
status: 200
content:
url: http://.../api/tts_proxy/....mp3
If you don’t get this, troubleshoot the TTS-generation layer before continuing.
Test 4 — Copy the Generated File
Take the generated URL and run:
action: shell_command.copy_tts_to_media
data:
source_url: "YOUR_GENERATED_TTS_URL"
filename: "tts-test.mp3"
response_variable: copy_test
Expected:
stdout: /media/tts_announcements/tts-test.mp3
stderr: ""
returncode: 0
SSH into Home Assistant:
ls -lh /media/tts_announcements/
You should see:
tts-test.mp3
You can also inspect it:
file /media/tts_announcements/tts-test.mp3
Test 5 — Play the Copied File
Finally:
action: media_player.play_media
target:
entity_id: media_player.your_homepod
data:
media_content_id: >
media-source://media_source/local/tts_announcements/tts-test.mp3
media_content_type: audio/mpeg
If this works, you have independently proven:
TTS generation ✓
TTS proxy output ✓
Shell command ✓
curl download ✓
/media storage ✓
Media Source ✓
Apple TV integration ✓
HomePod playback ✓
At that point, putting the pieces together in an automation is relatively straightforward.
Useful Debugging
For Apple TV/HomePod troubleshooting, additional logging can be useful:
logger:
logs:
pyatv: debug
homeassistant.components.apple_tv: debug
For the copy process, inspect the shell_command response:
stdout:
stderr:
returncode:
A successful copy should have:
returncode: 0
You can also SSH into Home Assistant and watch the media directory while testing:
watch -n 1 'ls -lh /media/tts_announcements'
During generation/copying, you may briefly see:
announcement_xxx.mp3.tmp
followed by:
announcement_xxx.mp3
That confirms the atomic copy process is doing what it is supposed to do.
Why I Think This Works
This section is my technical interpretation based on testing rather than a claim about the exact root cause inside Home Assistant or pyatv.
The problematic path appears to couple several operations:
Generate potentially slow TTS
+
make that stream available
+
initialize AirPlay
+
HomePod retrieves/decodes it
within a relatively time-sensitive operation.
The behavior I observed strongly suggested that TTS generation time was an important factor:
First long-message attempt → fails
Same message shortly afterward → works
The workaround deliberately decouples the operations.
Phase 1 — Generate
Generate audio
Wait for completion
Confirm generation succeeded
Phase 2 — Stage
Download the finished MP3
Verify the file isn't empty
Atomically move it into /media
Phase 3 — Play
Give the HomePod a normal,
already-existing local-media object
The HomePod therefore doesn’t participate until there is a complete MP3 waiting for it.
In my testing, playback from /media was fast and reliable.
Final Notes
This is a workaround, not necessarily the permanent solution.
There has been ongoing work around Home Assistant’s Apple TV/HomePod audio behavior, so future versions of Home Assistant and/or pyatv may make this unnecessary.
That said, I think this architecture has value even beyond this particular bug.
It deliberately separates:
Speech generation
from:
Speaker playback
For longer dynamic announcements—AI summaries, calendar/travel announcements, morning briefings, news summaries, weather briefings, etc.—that separation can make automations easier to troubleshoot and more deterministic.
It also gives you a very clear troubleshooting boundary:
Did TTS generate successfully?
Did the MP3 copy successfully?
Can HA Local Media serve it?
Can the HomePod play it?
Rather than troubleshooting all four things simultaneously.
I hope this helps anyone running into the same HomePod/TTS behavior. If you’re testing this on a different HA installation type, TTS engine, or Apple device, it would be useful to hear whether the same approach works there as well.
Useful Home Assistant Documentation
-
Home Assistant Apple TV integration:
Apple TV - Home Assistant -
Home Assistant Shell Command integration:
Shell Command - Home Assistant -
Home Assistant Local Media setup:
Setting up local media sources - Home Assistant -
Home Assistant Media Source integration:
Media source - Home Assistant