Files
wecker/README.md
T
gurix a67f57c37e refactor: remove command field from GraphQL Alarm type
command is generated internally and has no informational value to API
consumers, so stop exposing it on getAlarms/getAlarm/setAlarm.
2026-06-22 10:04:48 +02:00

5.8 KiB

Arcade Button Alarm Clock

A unique Raspberry Pi-based alarm clock that makes sure you are fully awake before it turns off!

Instead of simply pressing a button to stop the alarm, this clock requires you to solve a short memory and attention puzzle. When the alarm rings, you press the button, and the built-in LED will blink a random number of times (between 1 and 7). You then have to press the button exactly that many times to confirm. Get it right, and the music stops. Get it wrong, and you'll have to try again!

Hardware Requirements

  • Raspberry Pi 4 Model B (2018)
  • 33mm Illuminated Arcade Button (e.g., from bastelgarage.ch)
  • External Speakers (connected via 3.5mm jack or a USB Soundcard for better audio quality)

Software Requirements

This project uses Python 3 and uv for dependency management. To set up the environment:

  1. Install uv if you haven't already:
    curl -LsSf https://astral.sh/uv/install.sh | sh
    
  2. The project contains a pyproject.toml file with all dependencies.
    uv sync
    

Wiring

Please refer to the arcade-button-wiring.md file for detailed instructions on how to connect the arcade button and its LED to the Raspberry Pi's GPIO pins.

Usage

  1. Make sure your audio file (Laid Back - Sunshine Reggae.mp3 or any other MP3 you prefer) is in the project directory, or set a custom path.
  2. Run the alarm clock script manually:
    # Use the default music file or the MUSIC_FILE environment variable
    python3 wecker.py
    
    # Or pass a custom music file directly
    python3 wecker.py --music-file /path/to/your/alarm.mp3
    
    The music file is resolved in this order: --music-file argument, MUSIC_FILE environment variable, default Laid Back - Sunshine Reggae.mp3.

Automating and Managing Alarms (GraphQL API)

Instead of manually editing your crontab, this project provides a simple GraphQL API to List, Get, Set, and Delete your alarms.

API Setup & Authentication

  1. Create a .env file in the project root to set up your Static API Key:
    cp .env.example .env
    # Edit .env and set your own secure API_KEY
    
  2. The API is managed as a systemd user service and starts automatically on boot. To manage it manually, you can use:
    systemctl --user status wecker-api.service
    systemctl --user restart wecker-api.service
    
    (If you want to run it manually instead: uv run uvicorn api.main:app --host 0.0.0.0 --port 8000)
  3. The API will be available at http://<YOUR_PI_IP>:8000/graphql (For Tailscale users: http://100.88.83.57:8000/graphql).

Authentication: All requests to the /graphql endpoint require a custom header: X-API-Key: <YOUR_API_KEY>

Security Considerations

  • Keep the API key secret. Treat it like a password: store it only in .env, rotate it periodically, and generate a strong key with python3 -c "import secrets; print(secrets.token_urlsafe(32))".
  • The API currently does not implement rate limiting. The safest deployment is to expose it only on your local network or through Tailscale. If you expose it to the internet, place it behind a reverse proxy (e.g., nginx, Caddy, or Traefik) that handles TLS and brute-force protection.
  • If you need built-in rate limiting, consider adding slowapi or a similar ASGI middleware later.

Example API Usage

Check if alarm is currently ringing:

query {
  isRinging
}

Returns true if the wecker alarm clock is currently active (playing music), false otherwise.

Get all alarms:

query {
  getAlarms {
    id
    cronExpression
    isEnabled
  }
}

Set an alarm:

mutation {
  setAlarm(
    cronExpression: "45 6 * * 1-5",
    isEnabled: true
  ) {
    id
    cronExpression
    isEnabled
  }
}

(Note: command is generated automatically by the API and is not accepted as an input parameter on setAlarm, to prevent command injection into the system crontab.)

Delete an alarm:

mutation {
  deleteAlarm(id: "YOUR-ALARM-ID")
}

Start the alarm immediately:

mutation {
  startRinging
}

Returns true if the alarm started, false if it was already ringing (ignored).

Stop the alarm immediately:

mutation {
  stopRinging
}

Returns true if the alarm was stopped, false if it wasn't ringing.

Example response for isRinging:

{
  "data": {
    "isRinging": true
  }
}

How the Puzzle Works

  1. Ringing: The music plays in an endless loop.
  2. Start: Press the arcade button once to start the puzzle. Wait 3 seconds.
  3. Observe: The arcade button's LED will blink between 1 and 7 times.
  4. Input: Press the button the exact number of times the LED blinked. Every press is confirmed by the LED lighting up.
  5. Wait: Stop pressing for 3 seconds to lock in your answer.
  6. Evaluation:
    • Correct: The music stops and the script exits.
    • Incorrect: The alarm waits a few seconds and generates a completely new sequence for you to solve.

Troubleshooting

Audio issues when running via Cron (ALSA: Couldn't open audio device: Unknown error 524): If the script runs perfectly from the terminal but crashes when triggered by cron, it might be trying to use an invalid audio output (like a disconnected HDMI port) because background jobs don't have the same environment variables as interactive sessions.

To force the system to use the 3.5mm headphone jack (Audio Jack), you can create a ~/.asoundrc file for the pi user with the following content:

pcm.!default {
  type asym
  playback.pcm {
    type plug
    slave.pcm "hw:2,0"
  }
}
ctl.!default {
  type hw
  card 2
}

(Note: Change hw:2,0 and card 2 to match your actual headphone jack card index, which you can find by running aplay -l).

Author

Markus Graf (info@marksugraf.ch)