Skip to content

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, default false. Only true writes input_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): add script.play_daily_briefing18 → 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_briefing to 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 shared speak_briefing primitive.
  • docs/automations.md (§20, MkDocs manual, --strict): document the on-demand briefing and last_called targeting.
  • 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 in tests/fixtures/entities.txt; notify.* are services, not entity refs, so untested by the suite (same as today's kitchen call).
  • Run make test before pushing; deploy via the normal scripts.yaml → CI → ha core restart path.

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.