On-demand "play my daily briefing" on the Echo that heard it (#58)¶
Date: 2026-07-03 Issue: #58 Status: Design approved — ready for implementation plan
Goal¶
Say "Alexa, play my daily briefing" on any Echo and have the daily briefing (weather / SoFi / sleep score / energy) play back on that same Echo — not always the kitchen. Modularize the briefing so the on-demand and morning paths share one source of truth.
Why now (context change since the issue was filed)¶
The issue (2026-06-30) called "the same Echo" the hard part: the old alexa_devices integration exposed no last-active-device signal, so it proposed a fragile event.<device>_voice_event heuristic. The #61 migration to alexa_media (2026-07-02) changed that: every Echo media_player now carries a last_called boolean attribute — true on the one Echo you most recently spoke to. That is the deterministic signal the issue was missing, so Option 1 (last-active Echo, one script, one Alexa routine) is now the clean choice over per-room routines.
Decisions (locked)¶
| Question | Decision |
|---|---|
| Which Echo to speak on | The Echo whose last_called == true (deterministic via alexa_media) |
| Content | Same as the morning briefing, from a single shared source (greeting is time-aware — see below) |
Fallback when no last_called | Kitchen Echo Show (matches the morning target) |
| On-demand overwrites the morning outcome tile? | No (set_outcome: false) |
Architecture — one shared primitive, two callers¶
Three scripts in scripts.yaml:
1. script.speak_briefing — internal single source of truth (NOT Alexa-exposed)¶
All logic currently inside speak_morning_briefing moves here (the weather.get_forecasts call, the full modular parts message builder, the logbook line, the TTS call, the outcome write), parameterized by three fields. The only content change from today's briefing is a time-aware greeting (below) replacing the hardcoded "Good morning." — everything after the greeting is byte-for-byte identical. Fields:
notify_service— the notify service to speak through, e.g.notify.alexa_media_office_echo. Called via a templated action:action: "{{ notify_service }}".media_player— the entity checked for the honest-outcome availability test.set_outcome— bool, defaultfalse. Onlytruewritesinput_text.morning_briefing_outcome.
The briefing text is byte-for-byte the current briefing, only relocated. This satisfies the issue's "single source" acceptance criterion and keeps the
57 honest-outcome logic in one place.¶
Sketch:
speak_briefing:
alias: "Speak Briefing"
icon: mdi:bullhorn-variant
fields:
notify_service:
description: "Notify service to speak through (e.g. notify.alexa_media_office_echo)"
media_player:
description: "media_player entity checked for the honest-outcome test"
set_outcome:
description: "true = write input_text.morning_briefing_outcome"
default: false
sequence:
- action: weather.get_forecasts # (unchanged)
target: { entity_id: weather.forecast_home }
data: { type: daily }
response_variable: wf
continue_on_error: true
- variables:
message: >
{# time-aware greeting replaces the old hardcoded 'Good morning.' —
HA clock is ET, so now().hour is local. #}
{% set h = now().hour %}
{% set greeting = 'Good morning.' if h < 12
else ('Good afternoon.' if h < 18 else 'Good evening.') %}
{# ... rest of the EXACT current parts builder, unchanged, with
parts[0] = greeting instead of the literal 'Good morning.' ... #}
- action: logbook.log
data:
name: "Daily Briefing"
message: "Speaking briefing — {{ media_player }} is {{ states(media_player) }}."
entity_id: "{{ media_player }}"
- action: "{{ notify_service }}"
data:
message: "{{ message }}"
data: { type: tts }
- if: "{{ set_outcome }}"
then:
- action: input_text.set_value
target: { entity_id: input_text.morning_briefing_outcome }
data:
value: >-
{{ 'briefed' if states(media_player) not in ['unavailable','unknown'] else 'echo_unavailable' }}
mode: single
2. script.speak_morning_briefing — thin wrapper (keeps Alexa exposure)¶
Keeps its alias/icon and existing Alexa exposure. Body reduces to one call; morning behavior is unchanged (kitchen target, outcome tile still updated):
speak_morning_briefing:
alias: "Speak Morning Briefing"
icon: mdi:bullhorn-variant
sequence:
- action: script.speak_briefing
data:
notify_service: notify.alexa_media_kitchen_echo_show
media_player: media_player.kitchen_echo_show
set_outcome: true
mode: single
3. script.play_daily_briefing — on-demand entry point (Alexa-exposed)¶
Resolves the target Echo from last_called, derives its notify service by slug, and calls the shared primitive with set_outcome: false:
play_daily_briefing:
alias: "Play Daily Briefing"
icon: mdi:bullhorn-variant
sequence:
- variables:
echoes:
- media_player.kitchen_echo_show
- media_player.office_echo
- media_player.master_bedroom_echo
- media_player.master_bathroom_echo
- media_player.family_room_echo
- media_player.front_room_echo
- media_player.garage_echo
target: >
{% set active = echoes | select('is_state_attr','last_called', true) | list %}
{{ active[0] if active | count > 0 else 'media_player.kitchen_echo_show' }}
notify_service: "{{ 'notify.alexa_media_' ~ target.split('.')[1] }}"
- action: script.speak_briefing
data:
notify_service: "{{ notify_service }}"
media_player: "{{ target }}"
set_outcome: false
mode: single
last_called is a per-device boolean; exactly one Echo is true at a time, and all are null right after a restart (→ fallback fires). Adding an Echo later = one line in echoes. The explicit list (rather than trusting the alexa_media platform filter) excludes the Samsung soundbars/TV that share the platform but never carry a voice last_called.
Slug mapping is safe: all 7 Echo media_player entities have name: null in the entity registry (never hand-renamed), so each media_player.<slug> derives from the Amazon device name and notify.alexa_media_<slug> follows the same slug (confirmed for kitchen by the working morning script).
Exposure, docs & user steps¶
- Alexa allowlist (live
/config/.storage/homeassistant.exposed_entities, deny-default per #55): addscript.play_daily_briefing→ 18 → 19 exposed scripts. Runtime edit (jq/HA UI) +cloud/alexa/sync(WS) + Alexa app re-discover. Not in repo config. - CLAUDE.md: add
play_daily_briefingto the Alexa Voice Exposure "Utility" row and bump the count; add a sentence to the §20 architecture note about the on-demand path and the sharedspeak_briefingprimitive. - docs/automations.md (§20, MkDocs manual,
--strict): document the on-demand briefing andlast_calledtargeting. - User UI step (cannot be automated): in the Alexa app, create a custom routine — "play my daily briefing" → Activate scene → Play Daily Briefing. (HA exposes scripts to Alexa as scenes.) Spelled out for the user at hand-off.
Testing¶
- Structural (Tier 2): the new scripts add unique ids/aliases.
- Entity refs (Tier 3): the 7 Echo
media_players are all already intests/fixtures/entities.txt;notify.*are services, not entity refs, so untested by the suite (same as today's kitchen call). - Run
make testbefore pushing; deploy via the normalscripts.yaml→ CI →ha core restartpath.
Greeting is time-aware¶
The shared briefing opens with a greeting chosen from the local (ET) clock: "Good morning." before noon, "Good afternoon." until 6pm, "Good evening." after. The morning briefing is unaffected in practice (it runs in the morning → "Good morning."); an on-demand call at 3pm says "Good afternoon." Everything after the greeting is identical across both callers.
Acceptance (from the issue)¶
- [ ] "Alexa, play my daily briefing" on any Echo plays the current briefing on that Echo (fallback: kitchen).
- [ ] Briefing text modularized — single source shared with the morning briefing.
- [ ] Exposed to Alexa; CLAUDE.md exposure index + docs/automations.md §20 updated.