Back to blog
FILE 0x53·A JARVIS-VOICED MORNING BRIEFING ON THE ECHO STUDIO

A JARVIS-voiced morning briefing on the Echo Studio

May 8, 2026 · homelab, automation, alexa

I wanted something to talk at me when I get up — weather, todos, the date, sober days, mortgage-due reminder, homelab status. Not when an alarm fires, because I don't use one. When I actually get up. The pipeline detects wake-up from motion data, generates a short briefing in a JARVIS-butler voice, and announces it on the Echo Studio in the living room.

The flow

systemd timer (every 10 min, 04:00–10:00 local)
  → wake_detect.py --check
       Query motion-activity table for any qualifying activity since
       today 03:00 local. Ignore stationary/heartbeat events. If any
       qualifying activity OR it's past 09:00, proceed.
  → morning_briefing.generate_briefing(play=True)
       aggregate() pulls the day's context from a dashboard JSON
       endpoint (date, weather, todos, sober days, homelab status,
       next-bill-due)
       write_copy() runs a Sonnet call with a JARVIS-butler system
       prompt and ~150 word target
  → alexa_tts.speak(text, device="Echo Studio", method="announce")
  → mark date as fired in a flag file so it doesn't repeat

The timer fires every 10 minutes during a 6-hour wake window. Once a briefing fires for the day, the flag file blocks further runs. If nothing detects motion, the 09:00 (failsafe) tick announces anyway.

Talking to Alexa from a Linux server

There's no first-party API. The community library alexapy reverse- engineers the same endpoints the Alexa mobile app uses, including the refresh-token flow.

Once it's got a refresh token cached, scripted TTS looks like:

from alexa_tts import speak
speak("Good morning. The weather today is...",
      device="Echo Studio",
      method="announce")

There's a _DeviceShim class that wraps the raw device dict so AlexaAPI sees the attributes it expects (_locale, _device_type, _device_family, _cluster_members). Without that shim it crashes on attribute access.

The refresh token is durable across months but does eventually expire. When the briefing stops, the recovery procedure is "open the auth-capture-proxy URL, log in, get a new refresh token, drop it back into the creds file."

The gotcha that ate three hours

The briefing pipeline returned 200 OK from /api/behaviors/preview. The payload was correct. set_volume(80) worked. play_music('TUNEIN', 'NPR') actually played NPR on the Echo Studio at the right volume. The audio path was demonstrably fine.

But send_announcement and send_sequence("Alexa.Speak") returned 200 and produced zero sound. No chime, no speech, nothing.

Root cause: the Alexa app on my phone had Communications/ Announcements disabled for that specific Echo Studio. Routines and announcements use a different skill from music playback. The skill is gated by the per-device Communications enablement.

Three settings to flip in the Alexa app:

  1. More → Settings → Communications: master toggle ON
  2. Devices → Echo Studio → Communications: enable for that device
  3. Settings → Notifications → Announcements: ON

Same call started chiming and speaking through the Echo Studio immediately.

What I'd do differently

If the briefing ever goes silent again and the API still returns 200, the first thing to check is those app settings, not the credentials. I lost real time assuming "200 means it worked." Now I keep a smoke-test script that calls all three things — set_volume, play_music, and send_announcement — and reports which ones actually produce sound, so I can isolate which class of problem I'm dealing with before I touch the auth flow.

The other thing: send_tts in alexapy is non-functional because of an Amazon API limitation, so don't bother trying it. Use send_announcement and accept the soft chime that precedes the speech. The chime is actually nice — it's a little "incoming butler" sound before the voice.