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.