Wake Alarm — sunrise alarms with multi-room music, notifications, and a card

Hi all — I've been building Wake Alarm, a Home Assistant integration for sunrise wake-ups: lights ramp from warm-and-dim to cool-and-bright in the run-up to your alarm time, then music fades in at alarm time. Multi-instance, multi-room, with a custom Lovelace card.

Repo: GitHub - scootaash/hass-wake-alarm: Sunrise wake-up alarms for Home Assistant. Lights ramp up before alarm time, music fades in at alarm time. Multi-instance, multi-room, custom Lovelace card included. · GitHub

Alarm Card:

Setup Card:

Origin

I started with steku's parabolic_alarm blueprint — a great blueprint which I've been waking up to for a while. The light curve and the pairing a light ramp with audio replaced an old sunrise lamp I had. My setup outgrew what a single blueprint can comfortably do:

  • Separate alarms for weekday vs weekend, in different rooms
  • Multi-room Sonos playback rather than a single speaker
  • A card on the dashboard to tweak time / days / volume without editing YAML
  • Notification fallbacks for when a speaker is asleep and the music silently doesn't play (the "lights came on but I missed work" scenario, because the Sonos speaker dropped out)
  • Persistent media picker — pick a Sonos favourite from the card rather than typing a FV:2/3 ID into a blueprint input

Each of those was awkward to bolt onto a blueprint, so I've rewritten (with a lot of help from Claude) the underlying logic from scratch as a proper custom integration + Lovelace card. The ramp math, Sonos UPnP quirks, and snooze/dismiss flows are all carried forward from the same intent.

What it does

  • Lights ramp linearly from warm + dim to cool + bright over a configurable duration, ending at your alarm time. 20 steps/min by default;
  • Never dims a light below its current brightness, so if you brighten the room mid-ramp it stays.
  • Music fades in from 0 to a configured volume at alarm time, with a random 1–4 track-skip so each fire (and each snooze) starts on a different track from your favourite — not always track 1.
  • Multi-room playback for speakers that advertise the GROUPING feature (Sonos tested, with all the unjoin / 3-second wait / volume-zero / join / shuffle / settle dance baked in).
  • Mobile notifications with standard and urgent (critical) paths — distinct iOS interruption-levels and Android channels so you can give them different sounds.
  • Standard gives a phone option to click snooze
  • Critical bypasses Do Not Disturb if the speaker fails to play or no media has been picked yet.
  • Optional presence guard: skip the alarm silently if a configured person isn't home (so my wife isn't irritated to be woken up when I'm away)
  • Snooze, dismiss, cancel ramp — from the card, the mobile notification action buttons, or wake_alarm.* services.
  • Custom Lovelace card with day-of-week toggles, time picker, and an in-card media browser. Single HACS install (Integration only) — the card ships inside the integration and auto-registers as a Lovelace resource.

Tested with (only)

  • Philips Hue + Sonos
  • HA OS, current build
  • iOS Companion app

Not tested on Android, not tested with other light / media-player integrations. The integration uses standard HA service calls (light.turn_on, media_player.play_media, etc.) so it should work beyond my setup — reports either way are welcome.

Install (custom repo)

  1. HACS → ⋮ → Custom repositories → add this URL as Integration
  2. Download → restart HA
  3. Settings → Devices & Services → Add Integration → Wake Alarm
  4. Add type: custom:wake-alarm-card to a dashboard

This is 0.4.0-beta.1 — you'll need "Show beta versions" enabled in HACS to see it. Full instructions and a "How it works" walkthrough are in the README.

What I'd love feedback on

  • Does it work - I'd recommend testing a few times before trying overnight
  • The default ramp parameters
  • Other light + speaker brands working (or not)
  • Card UX — anything confusing on first use?
  • Setup-wizard or read-me wording

Roadmap

Possible ideas:

  • One-off override (alarm just once, then auto-disable)
  • Skip-next-occurrence
  • Voice Assist intents ("snooze ten minutes")
  • TTS announcements (good morning, weather, calendar preview)
  • Calendar-aware skip (bank holidays etc.)
  • Weather-aware ramp adjustments

Where to ask things

  • Bug reports / concrete feature requests → GitHub Issues (forms
    with required fields)
  • How do I…, setup questions, sharing dashboards → here on the
    forum thread
  • Discussion → here, please

Thanks again to steku for the blueprint. Looking forward to hearing how it goes for everyone.

looks great!
Very interested in the music fade in and the card!

I run a sunrise wake up alarm myself

a workday sensor (or any sensor) is a must to me to prevent the alarm from running on bank holidays

i would not overload in terms of features.
having the option to run a script before and after the alarm is perhaps wiser than trying to integrate an entire TTS greeting based on your agenda items since this is so personal; A real rabbit hole as well: my TTS has agenda items, traffic on the way to work, garbage notification but only the day before, weather, some local news items that i parsed from rss. Ideally you have a bell sound before the TTS message, you need to choose an AI to generate the text based on your info, another one to speak etc etc

This looks great, i like the idea to have it all in one place.
Is it possible to have a binary_sensor input, as i use a bed sensor so it doesn’t go off when iam not in bed?

Good shout - i agree, thats a much better idea than mixing two quite different things!

How would you see the workday sensor working - connected to a calendar?

Interesting - do you mean just a sensor that disables/enables it in the same way presence does, just giving more flexibility for connection?

Yes exactly that :smiley:.
And what was stated above is also a nice feature to have, i myself use the workday sensor that home assistant profide at the intergration page.

1 Like

It would be great if I didn’t have to use a media player, it literally won’t save without it.

I’m having trouble with this integration I have an alarm at 6:00 and length 30, it started ramping at 5:30 but at 5:59:52 it stopped and at 5:59:54 it started ramping from zero again for another 30 min almost it stopped at 6:29:47

The integration is controlling one philips hue bulb connected thru zigbee2mqtt.

Hi there. What are you after, a beep or just the lights?

I’ll take a look

Just lights, just would be nice if media player was optional

1 Like

This is great feedback – thank you for the detail

The way I had written this is that the light ramp triggers a series of steps that control the lighting step up, and then the last one triggers the lights.

The specific problem with this was that rounding in these steps means the trigger isn’t when the alarm expected to be, so the alarm doesn’t fire. Because I tested it on a light ramp of 15 minutes which wasn’t enough for the rounding to become a problem!

However, this made me think of a deeper problem, which is that it’s not sensible for the lights to trigger the alarm as any late related problem means the alarm doesn’t fire, which isn’t very robust.

I’m reworking it to separate the two time cycles, which should solve this problem and make the whole thing more effective.

Thanks again for trying it and for the specific comment which helped me easily get to the bottom of it

:sunrise: Wake Alarm 0.5.0

Hello — a quick note that 0.5.0 is out. It's a reasonably sizeable update, and a good deal of it came from suggestions and bug reports here and on GitHub, so thanks for those.

A few of the new bits:

  • :flashlight: / :musical_note:Lights and speakers are now each optional — a silent sunrise, a music-only alarm, or both.
  • :alarm_clock:Three script hooks — run your own automation before (the light ramp starts), at the alarm time itself (new — handy for a coffee machine, a bit of TTS, the blinds, and so on), and after (music ends / dismiss). They run non-blocking, so a slow script won't hold the alarm up.
  • :bed:Condition-sensor gate — only sound the alarm when, say, a bed sensor or the Workday sensor is on, alongside the existing presence check.
  • Single-speaker shuffle — a single speaker no longer starts on the same track every morning; it now shuffles and skips a random few tracks, as the multi-room path already did.
  • More robust scheduling — music runs on its own timer now, so a problem with the lights can't stop the alarm; plus restart catch-up and tidier DST handling.
  • Debug logging and a diagnostics download — to make troubleshooting and bug reports a little easier.

Full notes: Release 0.5.0 — More flexible alarms, script connections + a lots on reliability · scootaash/hass-wake-alarm · GitHub

Upgrading should hopefully be seamless via HACS — nothing to re-toggle, and your settings and media selection are kept. Minimum Home Assistant is 2024.6.

To install for now, add https://github.com/scootaash/hass-wake-alarm as a HACS custom repository (category Integration). I've also requested it be added to the HACS default store, though that's in their queue and may take a while, after which it'll show up in HACS directly.

Thanks @rob121, @arisig, @edi023 and @lawijter — a fair bit of this came from your ideas and reports.

As ever, bug reports are best on GitHub; questions and suggestions are welcome here.

Cheers.

3 Likes

I have open an issue regarding the card, looks like its not loaded.

Thank you for implementing the "bed sensor" condition.

i made a script with chatgpt that sumimulates the colors of the hue sunrise, maybe you can implement something like this.

blueprint:
  name: Wekker Zonlicht + Muziek Fade
  description: >
    Start een wekker-run met 3-fase XY zonsopkomst, Music Assistant playlist
    en volume fade.
  domain: script
  input:
    wake_lights:
      name: Wekverlichting
      selector:
        entity:
          domain: light
          multiple: true

    media_player_entity:
      name: Music Assistant media player
      selector:
        entity:
          domain: media_player

    playlist_media_id:
      name: Playlist media_id
      description: Bijvoorbeeld library://playlist/117
      default: "library://playlist/117"
      selector:
        text: {}

    alarm_active_helper:
      name: Alarm actief helper
      description: Bijvoorbeeld input_boolean.wekker_test_actief
      selector:
        entity:
          domain: input_boolean

    sunrise_duration_sec:
      name: Duur zonsopkomst
      default: 900
      selector:
        number:
          min: 60
          max: 3600
          step: 10
          unit_of_measurement: sec
          mode: box

    sunrise_steps:
      name: Aantal zonsopkomst-stappen
      default: 30
      selector:
        number:
          min: 1
          max: 120
          step: 1
          mode: box

    sunrise_start_brightness_pct:
      name: Start helderheid
      default: 1
      selector:
        number:
          min: 1
          max: 100
          step: 1
          unit_of_measurement: "%"
          mode: slider

    sunrise_end_brightness_pct:
      name: Eind helderheid
      default: 100
      selector:
        number:
          min: 1
          max: 100
          step: 1
          unit_of_measurement: "%"
          mode: slider

    start_volume:
      name: Beginvolume
      default: 0.08
      selector:
        number:
          min: 0
          max: 1
          step: 0.01
          mode: slider

    end_volume:
      name: Eindvolume
      default: 0.35
      selector:
        number:
          min: 0
          max: 1
          step: 0.01
          mode: slider

    volume_fade_duration_sec:
      name: Duur volumefade
      default: 900
      selector:
        number:
          min: 10
          max: 3600
          step: 10
          unit_of_measurement: sec
          mode: box

    volume_steps:
      name: Aantal volume-stappen
      default: 30
      selector:
        number:
          min: 1
          max: 120
          step: 1
          mode: box

mode: restart

sequence:
  - variables:
      wake_lights: !input wake_lights
      media_player_entity: !input media_player_entity
      playlist_media_id: !input playlist_media_id

      sunrise_duration_sec: !input sunrise_duration_sec
      sunrise_steps: !input sunrise_steps
      sunrise_start_brightness_pct: !input sunrise_start_brightness_pct
      sunrise_end_brightness_pct: !input sunrise_end_brightness_pct

      start_volume: !input start_volume
      end_volume: !input end_volume
      volume_fade_duration_sec: !input volume_fade_duration_sec
      volume_steps: !input volume_steps

      sunrise_step_delay: >-
        {% set steps = sunrise_steps | int(1) %}
        {% set duration = sunrise_duration_sec | int(0) %}
        {{ [1, (duration / steps) | round(0) | int(1)] | max }}

      volume_step_delay: >-
        {% set steps = volume_steps | int(1) %}
        {% set duration = volume_fade_duration_sec | int(0) %}
        {{ [1, (duration / steps) | round(0) | int(1)] | max }}

      volume_step_size: >-
        {% set steps = volume_steps | int(1) %}
        {{ ((end_volume | float(0)) - (start_volume | float(0))) / steps }}

  - action: input_boolean.turn_on
    target:
      entity_id: !input alarm_active_helper

  - parallel:
      - sequence:
          - action: light.turn_on
            target:
              entity_id: "{{ wake_lights }}"
            data:
              brightness_pct: "{{ sunrise_start_brightness_pct }}"
              xy_color: [0.27, 0.20]

          - repeat:
              count: "{{ sunrise_steps }}"
              sequence:
                - variables:
                    step: "{{ repeat.index }}"
                    total: "{{ sunrise_steps | int(30) }}"
                    progress: "{{ step / total }}"

                    phase: >-
                      {% if progress < 0.33 %}
                        1
                      {% elif progress < 0.66 %}
                        2
                      {% else %}
                        3
                      {% endif %}

                    phase_progress: >-
                      {% if progress < 0.33 %}
                        {{ progress / 0.33 }}
                      {% elif progress < 0.66 %}
                        {{ (progress - 0.33) / 0.33 }}
                      {% else %}
                        {{ (progress - 0.66) / 0.34 }}
                      {% endif %}

                    current_x: >-
                      {% if phase | int == 1 %}
                        {{ 0.27 + ((0.55 - 0.27) * phase_progress) }}
                      {% elif phase | int == 2 %}
                        {{ 0.55 + ((0.50 - 0.55) * phase_progress) }}
                      {% else %}
                        {{ 0.50 }}
                      {% endif %}

                    current_y: >-
                      {% if phase | int == 1 %}
                        {{ 0.20 + ((0.35 - 0.20) * phase_progress) }}
                      {% elif phase | int == 2 %}
                        {{ 0.35 + ((0.45 - 0.35) * phase_progress) }}
                      {% else %}
                        {{ 0.45 }}
                      {% endif %}

                    current_brightness: >-
                      {{
                        (sunrise_start_brightness_pct | float(1)) +
                        (
                          progress *
                          (
                            (sunrise_end_brightness_pct | float(100)) -
                            (sunrise_start_brightness_pct | float(1))
                          )
                        )
                      }}

                - delay:
                    seconds: "{{ sunrise_step_delay }}"

                - action: light.turn_on
                  target:
                    entity_id: "{{ wake_lights }}"
                  data:
                    brightness_pct: "{{ [current_brightness | float, sunrise_end_brightness_pct | float] | min }}"
                    xy_color:
                      - "{{ current_x | float }}"
                      - "{{ current_y | float }}"

      - sequence:
          - action: media_player.volume_set
            target:
              entity_id: "{{ media_player_entity }}"
            data:
              volume_level: "{{ start_volume }}"

          - action: music_assistant.play_media
            target:
              entity_id: "{{ media_player_entity }}"
            data:
              media_id: "{{ playlist_media_id }}"
              media_type: playlist
              enqueue: replace

          - repeat:
              count: "{{ volume_steps }}"
              sequence:
                - delay:
                    seconds: "{{ volume_step_delay }}"
                - action: media_player.volume_set
                  target:
                    entity_id: "{{ media_player_entity }}"
                  data:
                    volume_level: >-
                      {% set next_volume = (start_volume | float(0)) + (repeat.index * (volume_step_size | float(0))) %}
                      {{ [next_volume, end_volume | float(0)] | min }}