⏰

music-alarm-clock

A Raspberry Pi alarm clock that wakes you with Spotify or internet radio, controlled via a web UI and scheduled through crontab.

Flask :3141 Spotify API mpc/mpd crontab systemd

↻ Alarm Flow — How It Works

01
User sets time via
Web UI at /
➔
02
Crontab entry saved
with annotation tag
➔
03
Cron fires curl
to own server
➔
04
Volume fades in
via threaded amixer
➔
05
Music starts
Spotify Radio
➔
06
HA webhook
triggers smart home

The server curls itself — cron runs curl http://hostname:3141/radioalarm or /spotialarm. If Spotify fails, it falls back to radio automatically.

✶ Core Components

Flask Web Server

server.py

The heart of the project. A Flask app on port 3141 that exposes all alarm, playback, and configuration endpoints. Runs on 0.0.0.0 so it's reachable across the local network.

Key globals: alarm_time, alarm_active, currently_playing, fade_minutes

Alarm Scheduling

crontab via python-crontab

Alarms are stored as crontab entries tagged with "SPOTIFY ALARM". On save, old entries are removed and a new one is written. On startup, the server reads crontab to restore the active alarm.

The cron job is simply a curl to the server's own /radioalarm or /spotialarm endpoint.

Spotify Playback

spotipy + OAuth2

Uses the Spotify Web API via spotipy for playlist metadata and direct HTTP for playback control. Supports Authorization Code Flow with auto token refresh.

  • Random track offset from configured playlist
  • Fallback to radio on 404 (device not found)
  • Tokens stored in access_token.txt + config.ini
Internet Radio

mpc / mpd

Streams internet radio via MPD, controlled with mpc shell commands. Pre-configured with NTS Live streams.

  • mpc play/stop/next/prev/toggle
  • Stations added via make radio-stations
  • Acts as fallback when Spotify is unavailable
Volume Control

amixer with stereo balance

System volume via amixer with L/R channel balance using sqrt power curves. Fade-in/out runs in separate threads, incrementing 1% at a time over configurable minutes.

Default fade: 15 minutes to volume 30%.

Home Assistant

Webhook Integration

On alarm trigger, fires wakeup_alarm_triggered webhook to a local HA instance on port 8123. Enables smart home automations (lights, blinds, etc.) tied to the alarm.

🔑 Spotify OAuth2 Flow

sequenceDiagram
    participant U as User Browser
    participant S as Flask :3141
    participant SP as Spotify API

    U->>S: GET /login
    S-->>U: 302 Redirect to Spotify
    U->>SP: Authorize (scopes: playback)
    SP-->>U: Redirect to /authorize_spotify?code=...
    U->>S: GET /authorize_spotify?code=abc
    S->>SP: POST /api/token (code → tokens)
    SP-->>S: access_token + refresh_token
    S-->>U: "Tokens saved"

    Note over S,SP: Later — token expired
    S->>SP: POST /api/token (refresh_token)
    SP-->>S: New access_token
  

☸ API Routes

RouteMethodPurpose
/GETWeb UI — set alarm time, toggle on/off
/set_alarmPOSTSet alarm time (hour, minute) from form
/set_alarm_activePOSTToggle alarm on/off (JSON body)
/cronsavePOSTSave alarm to crontab with mode (radio/spotify)
/croncleanGETRemove all alarm cron entries
/radioalarmGETStart radio + fade in + HA webhook
/spotialarmGETStart Spotify + fade in + HA webhook
/radioplayGETPlay radio immediately
/radiostopGETStop radio
/radionext / /radioprevGETSwitch radio station
/spotiplayGETPlay random track from Spotify playlist
/spotipauseGETPause Spotify
/stopGETStop all playback (optional fade out)
/volume?volume=N&balance=BGETSet volume (0–100) and optional balance (-1 to 1)
/volumeup / /volumedownGETAdjust volume ±10%
/loginGETStart Spotify OAuth flow
/devicesGETList Spotify Connect devices
/areyourunningGETHealth check (also speaks via espeak)

📁 Project Structure

music-alarm-clock/
├── server.py                    — Flask app, all routes & logic
├── config_reader.py             — reads config.ini
├── config.ini                    — local config (gitignored)
├── config.sample.ini             — template for config
├── keyboard_input.py            — USB numpad controls
├── keyboard_control.py           — keyboard event handler
├── playlists.py                  — playlist definitions (unused)
├── utilities/                    — helper scripts
├── templates/
│   └── index.html                — web UI template
├── scripts/                      — misc shell scripts
├── test_*.py                     — pytest test files
├── Makefile                      — install, deps, radio, dev
├── Dockerfile                    — local dev container
├── spotify-alarm-clock.service  — systemd unit file
├── keyboard-controls.service    — systemd for numpad
├── requirements.txt              — Python dependencies
└── bash_aliases                  — convenience shell aliases

✎ Contributing Guide

Getting Started

  • Clone the repo
  • cp config.sample.ini config.ini
  • make dependencies
  • make docker-build && make docker-run for local dev
  • Visit localhost:3141

Key Patterns

  • All state is global variables in server.py
  • Spotify fallback: if Spotify fails, radio plays
  • Volume fades run in background threads
  • Crontab is the only persistence for alarms
  • Errors optionally read aloud via espeak

Watch Out For

  • Hardcoded path /home/pi/Developer/ in service file
  • Uses virtualenv not uv (legacy)
  • Race condition workaround in /volume
  • Bare except: blocks hide errors
  • No auth on any endpoint
This project runs on a Raspberry Pi. You can develop locally with Docker, but hardware-dependent features (amixer, mpc, espeak, GPIO keyboard) won't work outside a Pi.