I wanted a briefing when I walked into the office, not a fixed schedule one. The Home Assistant chain to do this is short on paper, easy to assemble, and surprisingly hard to land cleanly. The bug that ate three evenings turned out to have a one line fix, and the lesson was about how Cast abstractions wake the device for you when you let them.
This is the writeup I would have wanted before I started, with the YAML before and after, the dead ends I worked through, and the simple reason the working version is shorter than the broken one.
The chain
The pipeline has five pieces. A Zigbee contact sensor on the office door fires a state change when I open it. A calendar fetch picks up today’s events from a Google Calendar I keep for work. The OpenAI conversation integration takes that bundle and drafts a short briefing in my preferred tone. The TTS service tts.home_assistant_cloud, using the MiaNeural voice, turns the text into audio. The audio plays from a Nest Hub mounted on the wall above the desk.
Drawn as a text diagram, since plain text beats any image you would have to maintain:
[ office door opens ]
|
v
[ binary_sensor.office_door ] trigger
|
v
[ calendar.get_events ] today's events
|
v
[ conversation.process ] OpenAI drafts briefing
|
v
[ tts.home_assistant_cloud ] MiaNeural voice
|
v
[ Nest Hub on the wall ] audio out
Each step on its own works. The trigger fires reliably. The calendar fetch returns sensible JSON. The conversation step produces text I am happy to listen to. The TTS step renders audio. The Nest Hub plays audio when handed a media URL. The problem was always at the last hop.
The bug that ate three evenings
The symptom was media_player.turn_on timing out, sometimes intermittently, even when the device was already on. The briefing would generate, the TTS would render, then the action sequence would stall on the turn_on step, and the speech never reached the speaker. The next morning I would find a perfectly drafted TTS in the entity history with no audio output to match.
Sometimes it worked. On mornings I had used the Nest Hub to set a timer the night before, the briefing played fine. On mornings I had not touched the device in eight or more hours, the action would fail with Timeout waiting for media_player.office_hub to turn on. The state machine in Home Assistant said the device was on. The device itself responded to other commands. The turn_on call sat there for thirty seconds and quietly gave up.
This is the kind of bug that is easy to live with for a while, because the next failure is only ever one morning away, and each individual failure feels small. It is also exactly the kind of bug that earns a writeup, because the explanation is non-obvious and other people will hit it.
Things I tried that didn’t fix it
Increasing the wait_for_trigger timeout. The default is already generous. Pushing it higher just delayed the failure rather than changing it.
Adding a delay before turn_on. Half a second, two seconds, five seconds. None of them changed the outcome. The device was always already in the state I wanted it in by the time the call ran.
Splitting the action into separate scripts and chaining them together. Same failure, at a slightly different layer in the call stack, which mostly served to make the logs harder to read.
Reproducing it manually from Developer Tools while the device was idle. Sometimes it would fail there too, but inconsistently enough that I could not pin it down to a specific device state. That inconsistency is the thing that ate the most time, because intermittent bugs reward the wrong kind of patience.
Each of these moved me a little closer to suspecting the layer below media_player.turn_on, which is where the actual answer lived.
The actual fix
Drop media_player.turn_on entirely. The action sequence shrinks rather than grows.
Before:
action:
- service: media_player.turn_on
target:
entity_id: media_player.office_hub
- service: tts.speak
data:
cache: false
media_player_entity_id: media_player.office_hub
message: "{{ briefing_text }}"
target:
entity_id: tts.home_assistant_cloud
After:
action:
- service: tts.speak
data:
cache: false
media_player_entity_id: media_player.office_hub
message: "{{ briefing_text }}"
target:
entity_id: tts.home_assistant_cloud
One line removed. The morning after I shipped the change, the briefing played, and it has played every morning since.
Why this actually works
The Nest Hub is a Cast receiver. When tts.speak is called with a media_player_entity_id that resolves to a Cast device, the integration issues a Cast command with a media URL. The Cast protocol wakes the receiver as a side effect of starting media playback. No separate wake step is needed at the Cast layer, because the media start does the wake for free.
The media_player.turn_on service in Home Assistant maps to a different Cast call, the explicit wake, which the Nest Hub handles inconsistently when the device has been idle for a while. The two calls were racing each other inside the integration. On a cold receiver, the explicit wake would time out before the media start landed, and the action sequence would bail before TTS ever got the chance to play.
Looked at this way, the bug is structural rather than mysterious. Two layers were both trying to get the device into a playing state, and the lower layer already does the higher layer’s job for free. Adding the higher layer call was redundant, and in the cold receiver edge case it was actively harmful.
The general lesson is one I keep relearning. When two abstractions overlap, the lower one usually has the answer, and the win is deleting the higher call rather than reasoning harder about why it timed out.
What I’d add next, if I cared enough
Three small things, in roughly the order I would tackle them.
An evening briefing, triggered off door closure rather than door opening. I leave the office at the end of the day and a thirty second summary of tomorrow’s calendar would be useful. The work here is mostly prompt iteration, not plumbing.
A graceful fallback for slow calendar fetches. Once or twice a month the Google Calendar API takes ten or more seconds to respond, and the briefing arrives well after I have already sat down at the desk. A short timeout that drops back to a generic “no calendar today” line would beat the current behaviour, where a slow API silently shifts the briefing into the wrong slot of my morning.
Per-person briefings keyed off who is at home. The presence detection already exists in the system. The routing is straightforward: if I am in the office, briefing for me, if my partner is, briefing for her. This one needs more taste than engineering, which is why I have not done it yet.
The wider point
Home Assistant debugging is, more often than not, layered abstractions racing each other. The temptation is always to add a step. Wait for the device. Add a delay. Force the state. Most of the time the working version is the one with fewer steps rather than more. The discipline is noticing that the call below the one you are debugging has already done the job, and that the right move is deleting the call above it.
That has now happened to me three times this year on Home Assistant alone, in different domains, and I expect it to happen again before the year is out.