Skip to content

Ring Alarm — Whole-House Audio Feedback Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Give the Ring alarm whole-house spoken feedback across its full lifecycle (trigger, exit/entry delay, arm, disarm, failed-to-arm), every knob live-tunable from a dedicated dashboard with per-event test buttons.

Architecture: Three layers. script.alarm_speak sets Echo volume then Alexa-announces a message to a speaker list. script.alarm_audio_event maps an event name → speakers + volume (from input_number helpers), gates on enable toggles, and calls alarm_speak. Per-transition automations build the live message and call alarm_audio_event; a single test-button automation and the six input_buttons reuse the same path with force: true.

Tech Stack: Home Assistant YAML (automations, scripts, helpers), Jinja2 templates, alexa_media notify services, a YAML-mode Lovelace dashboard, pytest test suite (tests/).

Global Constraints

  • Spec: docs/superpowers/specs/2026-07-05-ring-alarm-audio-feedback-design.md.
  • Alarm panel entity: alarm_control_panel.blairmont_alarm (ring-mqtt). States: disarmed, armed_home, armed_away, arming, pending, triggered. Attributes: exitSecondsLeft, entrySecondsLeft, targetState.
  • The 7 Echoes (ALL_ECHOES): media_player.kitchen_echo_show, media_player.family_room_echo, media_player.front_room_echo, media_player.garage_echo, media_player.master_bathroom_echo, media_player.master_bedroom_echo, media_player.office_echo.
  • Announce mechanism: per-device notify.alexa_media_<object_id> with data.type: announce (matches the #61 migration; no dependency on a generic notify.alexa_media).
  • Helpers: define in configuration.yaml with NO initial: so dashboard tweaks persist across restarts (same convention as input_number.precool_high_threshold).
  • HA never arms/disarms/dispatches — this is observe-and-announce only. Do not add arm/disarm service calls.
  • Never edit automation_*.yaml fragments — all automations live in automations.yaml.
  • Commit style: Conventional Commits, scope alarm. End each commit body with Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>.
  • Test cycle: make lint && make pytest after every task (fast, no Docker). Run full make test (adds check-config, needs Docker) once before the final push (Task 8).
  • Branch: all work on feat/ring-alarm-audio (already checked out).

Task 1: Tunable helpers + allowlist forward-references

Files: - Modify: configuration.yaml (append to input_boolean: and input_number:; add a new input_button: top-level section) - Modify: tests/fixtures/entities_allow.txt

Interfaces: - Produces: 7 booleans input_boolean.alarm_audio_enabled, input_boolean.alarm_audio_{trigger,exit,entry,armed,disarmed,failed}_enabled; 8 numbers input_number.alarm_audio_{trigger,exit,entry,armed,disarmed,failed}_volume, input_number.alarm_audio_reset_volume, input_number.alarm_audio_trigger_interval; 6 buttons input_button.alarm_audio_test_{trigger,exit,entry,armed,disarmed,failed}.

  • [ ] Step 1: Append the enable booleans to the existing input_boolean: block in configuration.yaml
  # ── Ring Alarm audio feedback (see docs/…/2026-07-05-ring-alarm-audio-feedback-design.md)
  # Live on/off switches surfaced on the "Alarm Audio" dashboard. NO `initial:` so the
  # dashboard state persists across restarts (restore_state); flip them on once after deploy.
  alarm_audio_enabled:
    name: "Alarm Audio  Master"
    icon: mdi:bullhorn
  alarm_audio_trigger_enabled:
    name: "Alarm Audio  Trigger"
    icon: mdi:bullhorn-alert
  alarm_audio_exit_enabled:
    name: "Alarm Audio  Exit Delay"
    icon: mdi:exit-run
  alarm_audio_entry_enabled:
    name: "Alarm Audio  Entry Delay"
    icon: mdi:location-enter
  alarm_audio_armed_enabled:
    name: "Alarm Audio  Armed"
    icon: mdi:shield-check
  alarm_audio_disarmed_enabled:
    name: "Alarm Audio  Disarmed"
    icon: mdi:shield-off
  alarm_audio_failed_enabled:
    name: "Alarm Audio  Failed to Arm"
    icon: mdi:shield-alert
  • [ ] Step 2: Append the volume/interval numbers to the existing input_number: block
  # ── Ring Alarm audio feedback: per-event announce volumes (0–1) + trigger repeat interval.
  # NO `initial:` so dashboard tuning persists; each starts at its `min` on first boot after
  # deploy — set to the design defaults (below, in the `name`) once from the Alarm Audio dashboard.
  alarm_audio_trigger_volume:
    name: "Trigger Volume (0.80)"
    icon: mdi:volume-high
    min: 0
    max: 1
    step: 0.05
    mode: slider
  alarm_audio_exit_volume:
    name: "Exit-Delay Volume (0.60)"
    icon: mdi:volume-medium
    min: 0
    max: 1
    step: 0.05
    mode: slider
  alarm_audio_entry_volume:
    name: "Entry-Delay Volume (0.80)"
    icon: mdi:volume-high
    min: 0
    max: 1
    step: 0.05
    mode: slider
  alarm_audio_armed_volume:
    name: "Armed Volume (0.30)"
    icon: mdi:volume-low
    min: 0
    max: 1
    step: 0.05
    mode: slider
  alarm_audio_disarmed_volume:
    name: "Disarmed Volume (0.30)"
    icon: mdi:volume-low
    min: 0
    max: 1
    step: 0.05
    mode: slider
  alarm_audio_failed_volume:
    name: "Failed-to-Arm Volume (0.60)"
    icon: mdi:volume-medium
    min: 0
    max: 1
    step: 0.05
    mode: slider
  alarm_audio_reset_volume:
    name: "Echo Reset Volume (0.40)"
    icon: mdi:volume-source
    min: 0
    max: 1
    step: 0.05
    mode: slider
  alarm_audio_trigger_interval:
    name: "Trigger Repeat Interval (12s)"
    icon: mdi:timer-sync
    min: 5
    max: 60
    step: 1
    unit_of_measurement: "s"
    mode: box
  • [ ] Step 3: Add a new top-level input_button: section to configuration.yaml (there is none yet — place it right after the input_number: block ends)
##############################################################################
# INPUT BUTTONS
##############################################################################
input_button:
  # Alarm Audio dial-in: each press previews that event's announcement on its
  # assigned speakers at its current volume, bypassing the enable toggles.
  alarm_audio_test_trigger:
    name: "Test  Trigger Announcement"
    icon: mdi:play-circle
  alarm_audio_test_exit:
    name: "Test  Exit Delay"
    icon: mdi:play-circle
  alarm_audio_test_entry:
    name: "Test  Entry Delay"
    icon: mdi:play-circle
  alarm_audio_test_armed:
    name: "Test  Armed"
    icon: mdi:play-circle
  alarm_audio_test_disarmed:
    name: "Test  Disarmed"
    icon: mdi:play-circle
  alarm_audio_test_failed:
    name: "Test  Failed to Arm"
    icon: mdi:play-circle
  • [ ] Step 4: Append the forward-references to tests/fixtures/entities_allow.txt (they won't be in the snapshot until the post-deploy make snapshot)
# Ring Alarm audio feedback helpers (configuration.yaml) — created on the next HA
# restart after deploy; fold into entities.txt with `make snapshot` post-deploy,
# then delete this block. Spec: docs/…/2026-07-05-ring-alarm-audio-feedback-design.md
input_boolean.alarm_audio_enabled
input_boolean.alarm_audio_trigger_enabled
input_boolean.alarm_audio_exit_enabled
input_boolean.alarm_audio_entry_enabled
input_boolean.alarm_audio_armed_enabled
input_boolean.alarm_audio_disarmed_enabled
input_boolean.alarm_audio_failed_enabled
input_number.alarm_audio_trigger_volume
input_number.alarm_audio_exit_volume
input_number.alarm_audio_entry_volume
input_number.alarm_audio_armed_volume
input_number.alarm_audio_disarmed_volume
input_number.alarm_audio_failed_volume
input_number.alarm_audio_reset_volume
input_number.alarm_audio_trigger_interval
input_button.alarm_audio_test_trigger
input_button.alarm_audio_test_exit
input_button.alarm_audio_test_entry
input_button.alarm_audio_test_armed
input_button.alarm_audio_test_disarmed
input_button.alarm_audio_test_failed
  • [ ] Step 5: Run lint + tests

Run: make lint && make pytest Expected: PASS (yamllint clean; no entity-ref regressions — the new helpers are defined but not yet referenced, so Tier 3 is unaffected this task).

  • [ ] Step 6: Commit
git add configuration.yaml tests/fixtures/entities_allow.txt
git commit -m "feat(alarm): add tunable helpers for whole-house alarm audio

7 enable toggles, 8 volume/interval numbers, 6 test buttons (no initial:
so dashboard tuning persists). Forward-ref'd in entities_allow.txt until
the post-deploy snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 2: Audio primitive scripts (alarm_speak, alarm_audio_event)

Files: - Modify: scripts.yaml (add two scripts near the end, before the corner-lamp scene block or at end of file)

Interfaces: - Consumes: helpers from Task 1; the 7 Echo media_players. - Produces: - script.alarm_speak(message: str, speakers: list[str], volume: float) — volume_set + per-device announce. - script.alarm_audio_event(event: str, message: str, force: bool=false) — enable-gates, resolves speakers+volume by event, calls alarm_speak. Valid event values: trigger, exit, entry, armed, disarmed, failed.

  • [ ] Step 1: Add script.alarm_speak to scripts.yaml
# ── Ring Alarm audio: low-level fan-out. Sets volume on the target Echoes, then
# announces `message` on each via its per-device alexa_media notify service
# (notify.alexa_media_<object_id>, data.type announce — the #61 migration path).
# The 0.4s delay lets volume_set settle before the announce fires.
alarm_speak:
  alias: "Alarm  Speak"
  icon: mdi:bullhorn
  fields:
    message:
      description: "Text to announce"
      example: "Alarm disarmed."
    speakers:
      description: "List of Echo media_player entity_ids"
      example: "['media_player.master_bedroom_echo']"
    volume:
      description: "0.0–1.0 volume set before announcing"
      example: 0.6
  mode: parallel
  max: 10
  sequence:
    - action: media_player.volume_set
      target:
        entity_id: "{{ speakers }}"
      data:
        volume_level: "{{ volume | float(0.4) }}"
    - delay: "00:00:00.4"
    - repeat:
        for_each: "{{ speakers }}"
        sequence:
          - action: "notify.alexa_media_{{ repeat.item.split('.')[1] }}"
            data:
              message: "{{ message }}"
              data:
                type: announce
  • [ ] Step 2: Add script.alarm_audio_event to scripts.yaml (immediately after alarm_speak)
# ── Ring Alarm audio: mid-level policy. Maps an event name → speaker set + volume
# helper, gates on the master + per-event enable toggles (bypassed by force: true
# for the dashboard test buttons), then calls script.alarm_speak. Single source of
# the per-event speaker assignment.
alarm_audio_event:
  alias: "Alarm  Audio Event"
  icon: mdi:bullhorn-variant
  fields:
    event:
      description: "One of: trigger, exit, entry, armed, disarmed, failed"
      example: disarmed
    message:
      description: "Pre-built text to speak (caller fills live vars)"
      example: "Alarm disarmed."
    force:
      description: "true = bypass enable toggles (test buttons)"
      default: false
  mode: parallel
  max: 10
  variables:
    speakers: >-
      {% set allx = ['media_player.kitchen_echo_show','media_player.family_room_echo',
        'media_player.front_room_echo','media_player.garage_echo',
        'media_player.master_bathroom_echo','media_player.master_bedroom_echo',
        'media_player.office_echo'] %}
      {% set bedoff = ['media_player.master_bedroom_echo','media_player.office_echo'] %}
      {% set m = {'trigger': allx, 'exit': ['media_player.front_room_echo'],
        'entry': allx, 'armed': bedoff, 'disarmed': bedoff, 'failed': bedoff} %}
      {{ m.get(event, bedoff) }}
    vol: "{{ states('input_number.alarm_audio_' ~ event ~ '_volume') | float(0.4) }}"
    ev_on: "{{ is_state('input_boolean.alarm_audio_' ~ event ~ '_enabled', 'on') }}"
    master_on: "{{ is_state('input_boolean.alarm_audio_enabled', 'on') }}"
  sequence:
    - condition: "{{ force or (master_on and ev_on) }}"
    - action: script.alarm_speak
      data:
        message: "{{ message }}"
        speakers: "{{ speakers }}"
        volume: "{{ vol }}"
  • [ ] Step 3: Run lint + tests

Run: make lint && make pytest Expected: PASS. Tier 3 resolves input_number.alarm_audio_*_volume / input_boolean.alarm_audio_* via the Task 1 allowlist; the 7 Echoes exist in entities.txt.

  • [ ] Step 4: Commit
git add scripts.yaml
git commit -m "feat(alarm): add alarm_speak + alarm_audio_event primitives

alarm_speak = volume_set + per-device alexa_media announce fan-out.
alarm_audio_event = event→speaker/volume map + enable-gating (force bypass
for test buttons). Single source of per-event speaker assignment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 3: Lifecycle automations + strip #50's old announce

Files: - Modify: automations.yaml (§22 block, ~lines 2775–2896): delete the single kitchen-announce action in #50; add six new automations.

Interfaces: - Consumes: script.alarm_audio_event (Task 2); input_number.alarm_audio_trigger_interval, input_number.alarm_audio_reset_volume (Task 1).

  • [ ] Step 1: In automation #50 ("Alarm — Ring Triggered Critical Alert"), delete ONLY the Alexa announce action (the - alias: "Alexa announce — kitchen Echo" step and its action/data — lines ~2833–2838). Leave the two critical pushes and the persistent notification untouched. The new Trigger Loop (below) replaces house-wide audio.

  • [ ] Step 2: Add the Trigger Loop automation (in §22, after #51 the Night Lights Beacon)

# 52. Alarm Audio — Trigger Loop (repeats the intrusion announcement house-wide)
- alias: "Alarm Audio  Trigger Loop"
  id: alarm_audio_trigger_loop
  description: >
    While alarm_control_panel.blairmont_alarm is `triggered`, re-announce the
    intrusion on all 7 Echoes every input_number.alarm_audio_trigger_interval
    seconds until cleared. Mirrors §51's beacon loop (mode: restart).
  triggers:
    - trigger: state
      entity_id: alarm_control_panel.blairmont_alarm
      to: triggered
  mode: restart
  max_exceeded: silent
  variables:
    zones: >
      {% set ns = namespace(z=[]) %}
      {% for e in ['binary_sensor.personnel_door', 'binary_sensor.foyer_motion_sensor'] %}
        {% if is_state(e, 'on') %}{% set ns.z = ns.z + [state_attr(e, 'friendly_name')] %}{% endif %}
      {% endfor %}
      {{ ns.z | join(', ') if ns.z else 'zone unknown — check the Ring app' }}
  actions:
    - repeat:
        while:
          - condition: state
            entity_id: alarm_control_panel.blairmont_alarm
            state: triggered
        sequence:
          - action: script.alarm_audio_event
            data:
              event: trigger
              message: "Security alert. The Ring alarm has been triggered. {{ zones }}."
          - delay:
              seconds: "{{ states('input_number.alarm_audio_trigger_interval') | int(12) }}"
  • [ ] Step 3: Add the Exit-Delay automation
# 53. Alarm Audio — Exit Delay (front room echo; boundary announce + 10s warning)
- alias: "Alarm Audio  Exit Delay"
  id: alarm_audio_exit_delay
  triggers:
    - trigger: state
      entity_id: alarm_control_panel.blairmont_alarm
      to: arming
  mode: restart
  variables:
    target: "{{ 'home' if is_state_attr('alarm_control_panel.blairmont_alarm','targetState','armed_home') else 'away' }}"
    secs: "{{ state_attr('alarm_control_panel.blairmont_alarm','exitSecondsLeft') | int(60) }}"
  actions:
    - action: script.alarm_audio_event
      data:
        event: exit
        message: "Arming to {{ target }}. You have {{ secs }} seconds to exit."
    - delay:
        seconds: "{{ [secs - 10, 0] | max }}"
    - condition: state
      entity_id: alarm_control_panel.blairmont_alarm
      state: arming
    - action: script.alarm_audio_event
      data:
        event: exit
        message: "Ten seconds."
  • [ ] Step 4: Add the Entry-Delay automation
# 54. Alarm Audio — Entry Delay (all echoes: disarm-now prompt)
- alias: "Alarm Audio  Entry Delay"
  id: alarm_audio_entry_delay
  triggers:
    - trigger: state
      entity_id: alarm_control_panel.blairmont_alarm
      to: pending
  mode: restart
  actions:
    - action: script.alarm_audio_event
      data:
        event: entry
        message: "Entry delay. Disarm now."
  • [ ] Step 5: Add the Armed automation
# 55. Alarm Audio — Armed (bedroom + office echo confirmation)
- alias: "Alarm Audio  Armed"
  id: alarm_audio_armed
  triggers:
    - trigger: state
      entity_id: alarm_control_panel.blairmont_alarm
      to: armed_home
    - trigger: state
      entity_id: alarm_control_panel.blairmont_alarm
      to: armed_away
  variables:
    target: "{{ 'home' if trigger.to_state.state == 'armed_home' else 'away' }}"
  actions:
    - action: script.alarm_audio_event
      data:
        event: armed
        message: "Alarm armed to {{ target }}."
  • [ ] Step 6: Add the Disarmed automation (announces + resets all Echoes to the reset volume)
# 56. Alarm Audio — Disarmed (bedroom + office confirm; reset all echoes to reset volume)
- alias: "Alarm Audio  Disarmed"
  id: alarm_audio_disarmed
  triggers:
    - trigger: state
      entity_id: alarm_control_panel.blairmont_alarm
      to: disarmed
      from: armed_home
    - trigger: state
      entity_id: alarm_control_panel.blairmont_alarm
      to: disarmed
      from: armed_away
    - trigger: state
      entity_id: alarm_control_panel.blairmont_alarm
      to: disarmed
      from: pending
  actions:
    - action: script.alarm_audio_event
      data:
        event: disarmed
        message: "Alarm disarmed."
    - action: media_player.volume_set
      target:
        entity_id:
          - media_player.kitchen_echo_show
          - media_player.family_room_echo
          - media_player.front_room_echo
          - media_player.garage_echo
          - media_player.master_bathroom_echo
          - media_player.master_bedroom_echo
          - media_player.office_echo
      data:
        volume_level: "{{ states('input_number.alarm_audio_reset_volume') | float(0.4) }}"
  • [ ] Step 7: Add the Failed-to-Arm automation
# 57. Alarm Audio — Failed to Arm (arming aborted back to disarmed)
- alias: "Alarm Audio  Failed to Arm"
  id: alarm_audio_failed_arm
  description: >
    arming → disarmed = an exit-delay that aborted instead of reaching armed_*.
    Best-effort (a manual cancel during exit delay also fires this); revisit with a
    ring-mqtt fault attribute if false positives are common. See spec.
  triggers:
    - trigger: state
      entity_id: alarm_control_panel.blairmont_alarm
      to: disarmed
      from: arming
  actions:
    - action: script.alarm_audio_event
      data:
        event: failed
        message: "The alarm could not arm. Check doors and windows."
  • [ ] Step 8: Run lint + tests

Run: make lint && make pytest Expected: PASS. test_unique_automation_ids/aliases pass (new ids/aliases are unique); Tier 3 resolves all refs.

  • [ ] Step 9: Commit
git add automations.yaml
git commit -m "feat(alarm): whole-house audio for the alarm lifecycle

Six §22 automations — trigger loop (repeats house-wide), exit-delay (front
room + 10s warning), entry-delay, armed, disarmed (with all-echo volume
reset), failed-to-arm. Removes #50's single kitchen announce (superseded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 4: Test-button automation

Files: - Modify: automations.yaml (§22, after #57)

Interfaces: - Consumes: input_button.alarm_audio_test_* (Task 1); script.alarm_audio_event (Task 2).

  • [ ] Step 1: Add a single automation fanning all six test buttons into alarm_audio_event with force: true
# 58. Alarm Audio — Test Button Pressed (dial-in preview, bypasses enable toggles)
- alias: "Alarm Audio  Test Button Pressed"
  id: alarm_audio_test_button
  description: >
    Any input_button.alarm_audio_test_* press previews that event's announcement
    (force: true → ignores the enable toggles) with canned sample vars. Event name
    is the button's object_id after `alarm_audio_test_`.
  triggers:
    - trigger: state
      entity_id:
        - input_button.alarm_audio_test_trigger
        - input_button.alarm_audio_test_exit
        - input_button.alarm_audio_test_entry
        - input_button.alarm_audio_test_armed
        - input_button.alarm_audio_test_disarmed
        - input_button.alarm_audio_test_failed
  variables:
    event: "{{ trigger.entity_id.split('alarm_audio_test_')[1] }}"
    msgmap:
      trigger: "Security alert. The Ring alarm has been triggered. Foyer motion sensor."
      exit: "Arming to away. You have 60 seconds to exit."
      entry: "Entry delay. Disarm now."
      armed: "Alarm armed to home."
      disarmed: "Alarm disarmed."
      failed: "The alarm could not arm. Check doors and windows."
  actions:
    - action: script.alarm_audio_event
      data:
        event: "{{ event }}"
        force: true
        message: "{{ msgmap[event] }}"
  • [ ] Step 2: Run lint + tests

Run: make lint && make pytest Expected: PASS (unique id/alias; input_button.alarm_audio_test_* resolve via allowlist).

  • [ ] Step 3: Commit
git add automations.yaml
git commit -m "feat(alarm): test-button automation for audio dial-in

One automation maps the six alarm_audio_test_* button presses to
alarm_audio_event(force: true) with canned sample messages — preview any
event without arming the real alarm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 5: Alarm Audio dashboard + registration

Files: - Create: alarm_audio_dashboard.yaml (repo root) - Modify: configuration.yaml (lovelace.dashboards) - Modify: tests/conftest.py (dashboard refs map)

Interfaces: - Consumes: all Task 1 helpers.

  • [ ] Step 1: Create alarm_audio_dashboard.yaml (plain entities cards — no custom components; per-card title: is the standard card header, not a standalone title card)
title: Alarm Audio
views:
  - title: Alarm Audio
    path: default
    icon: mdi:bullhorn
    cards:
      - type: entities
        title: Master
        entities:
          - entity: input_boolean.alarm_audio_enabled
          - entity: input_number.alarm_audio_reset_volume
          - entity: input_number.alarm_audio_trigger_interval
      - type: entities
        title: Trigger
        entities:
          - entity: input_boolean.alarm_audio_trigger_enabled
          - entity: input_number.alarm_audio_trigger_volume
          - entity: input_button.alarm_audio_test_trigger
      - type: entities
        title: Exit Delay
        entities:
          - entity: input_boolean.alarm_audio_exit_enabled
          - entity: input_number.alarm_audio_exit_volume
          - entity: input_button.alarm_audio_test_exit
      - type: entities
        title: Entry Delay
        entities:
          - entity: input_boolean.alarm_audio_entry_enabled
          - entity: input_number.alarm_audio_entry_volume
          - entity: input_button.alarm_audio_test_entry
      - type: entities
        title: Armed
        entities:
          - entity: input_boolean.alarm_audio_armed_enabled
          - entity: input_number.alarm_audio_armed_volume
          - entity: input_button.alarm_audio_test_armed
      - type: entities
        title: Disarmed
        entities:
          - entity: input_boolean.alarm_audio_disarmed_enabled
          - entity: input_number.alarm_audio_disarmed_volume
          - entity: input_button.alarm_audio_test_disarmed
      - type: entities
        title: Failed to Arm
        entities:
          - entity: input_boolean.alarm_audio_failed_enabled
          - entity: input_number.alarm_audio_failed_volume
          - entity: input_button.alarm_audio_test_failed
  • [ ] Step 2: Register the dashboard under lovelace.dashboards in configuration.yaml (add after the command-center: block, ~line 37). url_path MUST contain a hyphen or check_config fails.
    alarm-audio:
      mode: yaml
      title: Alarm Audio
      icon: mdi:bullhorn
      show_in_sidebar: true
      filename: alarm_audio_dashboard.yaml
  • [ ] Step 3: Register the dashboard in the entity-ref test — add to the refs dict in tests/conftest.py all_referenced_entities() (after the command_dashboard.yaml line)
        # bare_scan: entities-card dashboard referencing helpers via `entity:` (singular),
        # which the entity_id:/states(...) regexes miss. All tokens are real helper ids.
        "alarm_audio_dashboard.yaml": extract_yaml_refs("alarm_audio_dashboard.yaml", bare_scan=True),
  • [ ] Step 4: Run lint + tests

Run: make lint && make pytest Expected: PASS. The new dashboard's helper refs resolve via the Task 1 allowlist. If bare_scan surfaces a stray non-entity token, add it to entities_allow.txt with a comment (mirrors the desk media_player.media_play_pause precedent) and re-run.

  • [ ] Step 5: Commit
git add alarm_audio_dashboard.yaml configuration.yaml tests/conftest.py
git commit -m "feat(alarm): Alarm Audio tuning dashboard (separate YAML dashboard)

New sidebar dashboard (url_path alarm-audio) with per-event enable toggle +
volume slider + test button, plus master toggle/reset-volume/interval.
Registered in lovelace.dashboards and the conftest entity-ref test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 6: User manual + CLAUDE.md docs

Files: - Modify: docs/notifications.md, docs/security.md, docs/automations.md, CLAUDE.md

Interfaces: none (docs only).

  • [ ] Step 1: docs/notifications.md — add a subsection listing the six spoken alarm announcements (trigger, exit, entry, armed, disarmed, failed-to-arm): what each says, which room speaks, when it fires. Note these are spoken on Echoes (not push).

  • [ ] Step 2: docs/security.md — add a "Whole-house alarm audio" section: the lifecycle announcements, the repeating trigger, and the Alarm Audio dashboard (enable toggles, volume sliders, test buttons) for tuning. Cross-link with a relative anchor to notifications.md if referenced (Material slug: lowercase, spaces→hyphens, punctuation stripped) so mkdocs build --strict passes.

  • [ ] Step 3: docs/automations.md — §22 technical reference: automations #52–#58, script.alarm_speak, script.alarm_audio_event, and the helper list.

  • [ ] Step 4: CLAUDE.md — (a) add Alarm Audio | yaml | alarm_audio_dashboard.yaml | alarm-audio to the dashboards table and the YAML-dashboard deploy list ("three YAML dashboards" → "four"); (b) under §22 in the Automations architecture list, note the audio primitive + six events + the tuning dashboard.

  • [ ] Step 5: Verify the docs build

Run: mkdocs build --strict (if mkdocs is available locally; otherwise rely on the docs.yml CI job post-push) Expected: build succeeds, no broken-link/anchor errors.

  • [ ] Step 6: Commit
git add docs/notifications.md docs/security.md docs/automations.md CLAUDE.md
git commit -m "docs(alarm): document whole-house alarm audio + tuning dashboard

notifications.md (spoken alerts), security.md (behavior + Alarm Audio
dashboard), automations.md (§22 #52–58 + scripts + helpers), CLAUDE.md
(dashboards table + §22 note).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 7: Full-suite verification (with container check_config)

Files: none (verification only).

  • [ ] Step 1: Run the complete pre-deploy suite (needs Docker for check-config)

Run: make test Expected: all tiers PASS — yamllint, JSON/Grafana guards, check_config on the pinned HA image (validates the new helpers, input_button: section, both scripts, seven automations, and the hyphenated alarm-audio url_path), unique-id/alias, entity refs.

  • [ ] Step 2: If check_config flags anything (e.g. a helper schema typo, an unknown service, or the dashboard filename), fix inline, re-run make test, and amend the relevant task's commit or add a fix(alarm): commit. Do not proceed until green.

Task 8: Merge to main, deploy, post-deploy snapshot

Files: tests/fixtures/entities.txt, tests/fixtures/entities_allow.txt (post-deploy).

  • [ ] Step 1: Merge the branch to main and push (this is what triggers CI deploy — the feature branch does not deploy)
git checkout main && git merge --no-ff feat/ring-alarm-audio
git push
  • [ ] Step 2: Verify the CI run (test job gates deploy; configuration.yaml + scripts.yaml changed → full ha core restart)

gh run list --workflow=ci.yml --limit 1
Expected: the run's test job passes and deploy-ha runs. Confirm HA came back: ssh haos "ha core logs --lines 20".

  • [ ] Step 3: Turn the audio on and set the design-default volumes ONCE (helpers boot at their min on first run). On the Alarm Audio dashboard: master ON + each event ON; set volumes trigger 0.80 / exit 0.60 / entry 0.80 / armed 0.30 / disarmed 0.30 / failed 0.60 / reset 0.40; interval 12. Then press each Test button to confirm the right rooms speak at a sane level. Validate open question #3 here (if announces come through at the previous volume, raise the delay in script.alarm_speak).

  • [ ] Step 4: Fold the helpers into the entity snapshot and drop the allowlist block

make snapshot   # needs HA_URL + HA_TOKEN — pulls the now-live helpers into entities.txt
Then delete the "Ring Alarm audio feedback helpers" block from tests/fixtures/entities_allow.txt.

  • [ ] Step 5: Run tests and commit the snapshot

Run: make lint && make pytest Expected: PASS with the helpers now sourced from entities.txt (allowlist block removed).

git add tests/fixtures/entities.txt tests/fixtures/entities_allow.txt
git commit -m "chore(alarm): fold audio helpers into entity snapshot

make snapshot after deploy; remove the forward-ref allowlist block now that
the helpers are in the live registry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
git push

Notes for the implementer

  • Open question #1 (fan-out): resolved by looping per-device notify.alexa_media_<object_id> in alarm_speak (Task 2) — no dependency on a generic notify.alexa_media. If the object-id→notify-slug mapping ever breaks for a renamed Echo, that's where to look.
  • Open question #2 (failed-to-arm): the arming → disarmed heuristic (Task 3 #57) is best-effort. After deploy, arm with a door open and watch alarm_control_panel.blairmont_alarm in Developer Tools → States; if ring-mqtt exposes a distinct fault attribute/state, prefer it and tighten #57.
  • Open question #3 (volume timing): the 0.4s delay in alarm_speak is the tunable knob; validate in Task 8 Step 3.
  • Speaker map is single-source in script.alarm_audio_event — to move which room speaks for an event, edit only that variables.speakers map, nowhere else.